diff --git a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts index aa49bda22b27e..28704086f6f3e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Babel/BabelPlugin.ts @@ -11,6 +11,7 @@ import { injectReanimatedFlag, pipelineUsesReanimatedPlugin, } from '../Entrypoint/Reanimated'; +import validateNoUntransformedReferences from '../Entrypoint/ValidateNoUntransformedReferences'; const ENABLE_REACT_COMPILER_TIMINGS = process.env['ENABLE_REACT_COMPILER_TIMINGS'] === '1'; @@ -67,6 +68,7 @@ export default function BabelPluginReactCompiler( comments: pass.file.ast.comments ?? [], code: pass.file.code, }); + validateNoUntransformedReferences(prog, opts.environment); if (ENABLE_REACT_COMPILER_TIMINGS === true) { performance.mark(`${filename}:end`, { detail: 'BabelPlugin:Program:end', diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts new file mode 100644 index 0000000000000..ae86a13cfa4e3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/ValidateNoUntransformedReferences.ts @@ -0,0 +1,188 @@ +import {NodePath} from '@babel/core'; +import * as t from '@babel/types'; + +import {CompilerError, EnvironmentConfig} from '..'; +import {getOrInsertWith} from '../Utils/utils'; +import {Environment} from '../HIR'; + +export default function validateNoUntransformedReferences( + path: NodePath, + env: EnvironmentConfig, +): void { + const moduleLoadChecks = new Map< + string, + Map + >(); + if (env.enableFire) { + /** + * Error on any untransformed references to `fire` (e.g. including non-call + * expressions) + */ + for (const module of Environment.knownReactModules) { + const react = getOrInsertWith(moduleLoadChecks, module, () => new Map()); + react.set('fire', assertNone); + } + } + if (env.inferEffectDependencies) { + /** + * Only error on untransformed references of the form `useMyEffect(...)` or + * `moduleNamespace.useMyEffect(...)`, with matching argument counts. + * TODO: add mode to also hard error on non-call references + */ + for (const { + function: {source, importSpecifierName}, + numRequiredArgs, + } of env.inferEffectDependencies) { + const module = getOrInsertWith(moduleLoadChecks, source, () => new Map()); + module.set( + importSpecifierName, + assertNoAutoDepCalls.bind(null, numRequiredArgs), + ); + } + } + if (moduleLoadChecks.size > 0) { + transformProgram(path, moduleLoadChecks); + } +} + +type TraversalState = { + shouldInvalidateScopes: boolean; + program: NodePath; +}; +type CheckInvalidReferenceFn = (paths: Array>) => void; +function validateImportSpecifier( + specifier: NodePath, + importSpecifierChecks: Map, + state: TraversalState, +): void { + const imported = specifier.get('imported'); + const specifierName: string = + imported.node.type === 'Identifier' + ? imported.node.name + : imported.node.value; + const checkFn = importSpecifierChecks.get(specifierName); + if (checkFn == null) { + return; + } + if (state.shouldInvalidateScopes) { + state.shouldInvalidateScopes = false; + state.program.scope.crawl(); + } + + const local = specifier.get('local'); + const binding = local.scope.getBinding(local.node.name); + CompilerError.invariant(binding != null, { + reason: 'Expected binding to be found for import specifier', + loc: local.node.loc ?? null, + }); + checkFn(binding.referencePaths); +} + +function validateNamespacedImport( + specifier: NodePath, + importSpecifierChecks: Map, + state: TraversalState, +): void { + if (state.shouldInvalidateScopes) { + state.shouldInvalidateScopes = false; + state.program.scope.crawl(); + } + const local = specifier.get('local'); + const binding = local.scope.getBinding(local.node.name); + + CompilerError.invariant(binding != null, { + reason: 'Expected binding to be found for import specifier', + loc: local.node.loc ?? null, + }); + const filteredReferences = new Map< + CheckInvalidReferenceFn, + Array> + >(); + for (const reference of binding.referencePaths) { + const parent = reference.parentPath; + if ( + parent != null && + parent.isMemberExpression() && + parent.get('object') === reference + ) { + if (parent.node.computed || parent.node.property.type !== 'Identifier') { + continue; + } + const checkFn = importSpecifierChecks.get(parent.node.property.name); + if (checkFn != null) { + getOrInsertWith(filteredReferences, checkFn, () => []).push(parent); + } + } + } + + for (const [checkFn, references] of filteredReferences) { + checkFn(references); + } +} +function transformProgram( + path: NodePath, + moduleLoadChecks: Map>, +): void { + const traversalState = {shouldInvalidateScopes: true, program: path}; + path.traverse({ + ImportDeclaration(path: NodePath) { + const importSpecifierChecks = moduleLoadChecks.get( + path.node.source.value, + ); + if (importSpecifierChecks == null) { + return; + } + const specifiers = path.get('specifiers'); + for (const specifier of specifiers) { + if (specifier.isImportSpecifier()) { + validateImportSpecifier( + specifier, + importSpecifierChecks, + traversalState, + ); + } else { + validateNamespacedImport( + specifier as NodePath< + t.ImportNamespaceSpecifier | t.ImportDefaultSpecifier + >, + importSpecifierChecks, + traversalState, + ); + } + } + }, + }); +} + +function assertNoAutoDepCalls( + numArgs: number, + paths: Array>, +): void { + for (const path of paths) { + const parent = path.parentPath; + if (parent != null && parent.isCallExpression()) { + const args = parent.get('arguments'); + if (args.length === numArgs) { + /** + * Note that we cannot easily check the type of the first argument here, + * as it may have already been transformed by the compiler (and not + * memoized). + */ + CompilerError.throwTodo({ + reason: + 'Untransformed reference to experimental compiler-only feature', + loc: parent.node.loc ?? null, + }); + } + } + } +} + +function assertNone(paths: Array>): void { + if (paths.length > 0) { + CompilerError.throwTodo({ + reason: 'Untransformed reference to experimental compiler-only feature', + loc: paths[0].node.loc ?? null, + }); + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 83138c596eaca..8ca57321d864e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -1121,6 +1121,7 @@ export class Environment { moduleName.toLowerCase() === 'react-dom' ); } + static knownReactModules: ReadonlyArray = ['react', 'react-dom']; getPropertyType( receiver: Type, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.expect.md new file mode 100644 index 0000000000000..cdf73a8f23c09 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.expect.md @@ -0,0 +1,26 @@ + +## Input + +```javascript +// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none) +import {useEffect} from 'react'; + +function nonReactFn(arg) { + useEffect(() => [1, 2, arg]); +} + +``` + + +## Error + +``` + 3 | + 4 | function nonReactFn(arg) { +> 5 | useEffect(() => [1, 2, arg]); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (5:5) + 6 | } + 7 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.js new file mode 100644 index 0000000000000..813c3b6c3dccb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.callsite-in-non-react-fn.js @@ -0,0 +1,6 @@ +// @inferEffectDependencies @compilationMode(infer) @panicThreshold(none) +import {useEffect} from 'react'; + +function nonReactFn(arg) { + useEffect(() => [1, 2, arg]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.expect.md new file mode 100644 index 0000000000000..53d8f65eef9d0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold(none) +import {useEffect} from 'react'; + +/** + * Error on non-inlined effect functions: + * 1. From the effect hook callee's perspective, it only makes sense + * to either + * (a) never hard error (i.e. failing to infer deps is acceptable) or + * (b) always hard error, + * regardless of whether the callback function is an inline fn. + * 2. (Technical detail) it's harder to support detecting cases in which + * function (pre-Forget transform) was inline but becomes memoized + */ +function Component({foo}) { + function f() { + console.log(foo); + } + + // No inferred dep array, the argument is not a lambda + useEffect(f); +} + +``` + + +## Error + +``` + 18 | + 19 | // No inferred dep array, the argument is not a lambda +> 20 | useEffect(f); + | ^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (20:20) + 21 | } + 22 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.js new file mode 100644 index 0000000000000..a011e3bf144e1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.non-inlined-effect-fn.js @@ -0,0 +1,21 @@ +// @inferEffectDependencies @panicThreshold(none) +import {useEffect} from 'react'; + +/** + * Error on non-inlined effect functions: + * 1. From the effect hook callee's perspective, it only makes sense + * to either + * (a) never hard error (i.e. failing to infer deps is acceptable) or + * (b) always hard error, + * regardless of whether the callback function is an inline fn. + * 2. (Technical detail) it's harder to support detecting cases in which + * function (pre-Forget transform) was inline but becomes memoized + */ +function Component({foo}) { + function f() { + console.log(foo); + } + + // No inferred dep array, the argument is not a lambda + useEffect(f); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.expect.md new file mode 100644 index 0000000000000..50f4caeaaec4a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.expect.md @@ -0,0 +1,27 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold(none) +import React from 'react'; + +function Component() { + const obj = makeObject_Primitives(); + React.useEffect(() => print(obj)); +} + +``` + + +## Error + +``` + 4 | function Component() { + 5 | const obj = makeObject_Primitives(); +> 6 | React.useEffect(() => print(obj)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (6:6) + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.js similarity index 59% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.js index 0dbae754ecf76..d2d6149941312 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.todo-import-default-property-useEffect.js @@ -1,7 +1,7 @@ -// @inferEffectDependencies +// @inferEffectDependencies @panicThreshold(none) import React from 'react'; -function NonReactiveDepInEffect() { +function Component() { const obj = makeObject_Primitives(); React.useEffect(() => print(obj)); } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.expect.md new file mode 100644 index 0000000000000..6c124ef5c7d56 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.expect.md @@ -0,0 +1,27 @@ + +## Input + +```javascript +// @inferEffectDependencies @panicThreshold(none) +import {useEffect} from 'react'; + +function Component({propVal}) { + 'use no memo'; + useEffect(() => [propVal]); +} + +``` + + +## Error + +``` + 4 | function Component({propVal}) { + 5 | 'use no memo'; +> 6 | useEffect(() => [propVal]); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Todo: Untransformed reference to experimental compiler-only feature (6:6) + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.js new file mode 100644 index 0000000000000..53eeda50501ed --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/error.use-no-memo.js @@ -0,0 +1,7 @@ +// @inferEffectDependencies @panicThreshold(none) +import {useEffect} from 'react'; + +function Component({propVal}) { + 'use no memo'; + useEffect(() => [propVal]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.expect.md index 89da346a72c50..d0745f9bd37ba 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.expect.md @@ -34,13 +34,6 @@ function Component({foo, bar}) { console.log(bar.qux); }); - function f() { - console.log(foo); - } - - // No inferred dep array, the argument is not a lambda - useEffect(f); - useEffectWrapper(() => { console.log(foo); }); @@ -58,7 +51,7 @@ import useEffectWrapper from "useEffectWrapper"; const moduleNonReactive = 0; function Component(t0) { - const $ = _c(14); + const $ = _c(12); const { foo, bar } = t0; const ref = useRef(0); @@ -119,7 +112,7 @@ function Component(t0) { useEffect(t4, [bar.baz, bar.qux]); let t5; if ($[10] !== foo) { - t5 = function f() { + t5 = () => { console.log(foo); }; $[10] = foo; @@ -127,20 +120,7 @@ function Component(t0) { } else { t5 = $[11]; } - const f = t5; - - useEffect(f); - let t6; - if ($[12] !== foo) { - t6 = () => { - console.log(foo); - }; - $[12] = foo; - $[13] = t6; - } else { - t6 = $[13]; - } - useEffectWrapper(t6, [foo]); + useEffectWrapper(t5, [foo]); } ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.js index efdd4164789b4..2bad5ee1cc920 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/infer-effect-dependencies.js @@ -30,13 +30,6 @@ function Component({foo, bar}) { console.log(bar.qux); }); - function f() { - console.log(foo); - } - - // No inferred dep array, the argument is not a lambda - useEffect(f); - useEffectWrapper(() => { console.log(foo); }); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.expect.md deleted file mode 100644 index a5a576c83067a..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/todo.import-default-property-useEffect.expect.md +++ /dev/null @@ -1,44 +0,0 @@ - -## Input - -```javascript -// @inferEffectDependencies -import React from 'react'; - -function NonReactiveDepInEffect() { - const obj = makeObject_Primitives(); - React.useEffect(() => print(obj)); -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies -import React from "react"; - -function NonReactiveDepInEffect() { - const $ = _c(2); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = makeObject_Primitives(); - $[0] = t0; - } else { - t0 = $[0]; - } - const obj = t0; - let t1; - if ($[1] === Symbol.for("react.memo_cache_sentinel")) { - t1 = () => print(obj); - $[1] = t1; - } else { - t1 = $[1]; - } - React.useEffect(t1); -} - -``` - -### Eval output -(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md new file mode 100644 index 0000000000000..5f323d486821b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @enableFire @panicThreshold(none) +import {fire} from 'react'; + +/** + * TODO: we should eventually distinguish between `use no memo` and `use no + * compiler` directives. The former should be used to *only* disable memoization + * features. + */ +function Component({props, bar}) { + 'use no memo'; + const foo = () => { + console.log(props); + }; + useEffect(() => { + fire(foo(props)); + fire(foo()); + fire(bar()); + }); + + return null; +} + +``` + + +## Error + +``` + 13 | }; + 14 | useEffect(() => { +> 15 | fire(foo(props)); + | ^^^^ Todo: Untransformed reference to experimental compiler-only feature (15:15) + 16 | fire(foo()); + 17 | fire(bar()); + 18 | }); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.js similarity index 51% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.js index 2587e24ee1170..92e1ff211a62c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/error.use-no-memo.js @@ -1,6 +1,11 @@ -// @enableFire +// @enableFire @panicThreshold(none) import {fire} from 'react'; +/** + * TODO: we should eventually distinguish between `use no memo` and `use no + * compiler` directives. The former should be used to *only* disable memoization + * features. + */ function Component({props, bar}) { 'use no memo'; const foo = () => { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.expect.md deleted file mode 100644 index 907501228b10b..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/transform-fire/bailout-retry/todo-use-no-memo.expect.md +++ /dev/null @@ -1,47 +0,0 @@ - -## Input - -```javascript -// @enableFire -import {fire} from 'react'; - -function Component({props, bar}) { - 'use no memo'; - const foo = () => { - console.log(props); - }; - useEffect(() => { - fire(foo(props)); - fire(foo()); - fire(bar()); - }); - - return null; -} - -``` - -## Code - -```javascript -// @enableFire -import { fire } from "react"; - -function Component({ props, bar }) { - "use no memo"; - const foo = () => { - console.log(props); - }; - useEffect(() => { - fire(foo(props)); - fire(foo()); - fire(bar()); - }); - - return null; -} - -``` - -### Eval output -(kind: exception) Fixture not implemented \ No newline at end of file