diff --git a/nixjs-rt/src/builtins/builtins.ts b/nixjs-rt/src/builtins.ts similarity index 63% rename from nixjs-rt/src/builtins/builtins.ts rename to nixjs-rt/src/builtins.ts index 7353e15..1f9dd03 100644 --- a/nixjs-rt/src/builtins/builtins.ts +++ b/nixjs-rt/src/builtins.ts @@ -1,3 +1,7 @@ +import { err, errType, errTypes } from "./errors"; +import { abortError } from "./errors/abort"; +import { otherError } from "./errors/other"; +import { typeMismatchError } from "./errors/typeError"; import { Attrset, EvalCtx, @@ -10,27 +14,41 @@ import { NixList, NixString, NixType, + NixTypeClass, Path, TRUE, -} from "../lib"; +} from "./lib"; type BuiltinsRecord = Record NixType>; +function builtinBasicTypeMismatchError( + fnName: string, + got: NixType, + expects: NixTypeClass | NixTypeClass[], +) { + if (!Array.isArray(expects)) { + expects = [expects]; + } + + return typeMismatchError( + got, + expects, + err`${fnName} expects ${errTypes(...expects)}, got ${errType(got)}.`, + ); +} + export function getBuiltins() { const builtins: BuiltinsRecord = { abort: (message) => { - throw new EvalException( - `Evaluation aborted with the following error message: '${message.asString()}'`, - ); + throw abortError(message.asString()); }, import: (path) => { const pathStrict = path.toStrict(); if (!(pathStrict instanceof Path || pathStrict instanceof NixString)) { - throw new EvalException( - `Cannot import a value of type '${pathStrict.typeOf()}'.`, - ); + const expected = [Path, NixString]; + throw builtinBasicTypeMismatchError("import", pathStrict, expected); } const pathValue = pathStrict.toJs(); @@ -47,15 +65,13 @@ export function getBuiltins() { return new Lambda((rhs) => { let lhsStrict = lhs.toStrict(); if (!(lhsStrict instanceof NixInt || lhsStrict instanceof NixFloat)) { - throw new EvalException( - `value is of type '${lhs.typeOf()}' while a number was expected.`, - ); + let expected = [NixInt, NixFloat]; + throw builtinBasicTypeMismatchError("add", lhsStrict, expected); } let rhsStrict = rhs.toStrict(); if (!(rhsStrict instanceof NixInt || rhsStrict instanceof NixFloat)) { - throw new EvalException( - `value is of type '${rhs.typeOf()}' while a number was expected.`, - ); + let expected = [NixInt, NixFloat]; + throw builtinBasicTypeMismatchError("add", rhsStrict, expected); } return lhsStrict.add(rhsStrict); }); @@ -64,14 +80,14 @@ export function getBuiltins() { head: (list) => { const listStrict = list.toStrict(); if (!(listStrict instanceof NixList)) { - throw new EvalException( - `Cannot apply the 'head' function on '${listStrict.typeOf()}'.`, + throw typeMismatchError( + listStrict, + NixList, + err`Cannot apply the 'head' function on '${errType(listStrict)}', expected ${errType(NixList)}.`, ); } if (listStrict.values.length === 0) { - throw new EvalException( - "Cannot fetch the first element in an empty list.", - ); + throw otherError("Cannot fetch the first element in an empty list."); } return listStrict.values[0]; }, @@ -79,17 +95,13 @@ export function getBuiltins() { all: (pred) => { const lambdaStrict = pred.toStrict(); if (!(lambdaStrict instanceof Lambda)) { - throw new EvalException( - `'all' function requires another function, but got '${lambdaStrict.typeOf()}' instead.`, - ); + throw builtinBasicTypeMismatchError("all", lambdaStrict, Lambda); } return new Lambda((list) => { const listStrict = list.toStrict(); if (!(listStrict instanceof NixList)) { - throw new EvalException( - `Cannot apply the 'all' function on '${listStrict.typeOf()}'.`, - ); + throw builtinBasicTypeMismatchError("all", listStrict, NixList); } for (const element of listStrict.values) { @@ -106,17 +118,13 @@ export function getBuiltins() { any: (pred) => { const lambdaStrict = pred.toStrict(); if (!(lambdaStrict instanceof Lambda)) { - throw new EvalException( - `'any' function requires another function, but got '${lambdaStrict.typeOf()}' instead.`, - ); + throw builtinBasicTypeMismatchError("any", lambdaStrict, Lambda); } return new Lambda((list) => { const listStrict = list.toStrict(); if (!(listStrict instanceof NixList)) { - throw new EvalException( - `Cannot apply the 'any' function on '${listStrict.typeOf()}'.`, - ); + throw builtinBasicTypeMismatchError("any", listStrict, NixList); } for (const element of listStrict.values) { @@ -133,8 +141,10 @@ export function getBuiltins() { attrNames: (attrset) => { const attrsetStrict = attrset.toStrict(); if (!(attrsetStrict instanceof Attrset)) { - throw new EvalException( - `Cannot apply the 'attrNames' function on '${attrsetStrict.typeOf()}'.`, + throw builtinBasicTypeMismatchError( + "attrNames", + attrsetStrict, + Attrset, ); } @@ -147,8 +157,10 @@ export function getBuiltins() { attrValues: (attrset) => { const attrsetStrict = attrset.toStrict(); if (!(attrsetStrict instanceof Attrset)) { - throw new EvalException( - `Cannot apply the 'attrValues' function on '${attrsetStrict.typeOf()}'.`, + throw builtinBasicTypeMismatchError( + "attrValues", + attrsetStrict, + Attrset, ); } diff --git a/nixjs-rt/src/builtins/tests/abort.test.ts b/nixjs-rt/src/builtins/tests/abort.test.ts deleted file mode 100644 index bad74d6..0000000 --- a/nixjs-rt/src/builtins/tests/abort.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.abort' throws with the given message", () => { - expect(() => getBuiltin("abort").apply(new NixString("foo"))).toThrow( - new EvalException( - "Evaluation aborted with the following error message: 'foo'", - ), - ); - // `abort` is special, since it's available directly on the global scope - expect(() => evalCtx().lookup("abort").apply(new NixString("foo"))).toThrow( - new EvalException( - "Evaluation aborted with the following error message: 'foo'", - ), - ); -}); - -test("'builtins.abort' on a non-string throws during coercion", () => { - expect(() => getBuiltin("abort").apply(new NixFloat(1))).toThrow( - new EvalException("Value is 'float' but a string was expected."), - ); -}); diff --git a/nixjs-rt/src/builtins/tests/add.test.ts b/nixjs-rt/src/builtins/tests/add.test.ts deleted file mode 100644 index efa12e7..0000000 --- a/nixjs-rt/src/builtins/tests/add.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.add' adds two numbers", () => { - expect( - getBuiltin("add").apply(new NixInt(1n)).apply(new NixInt(2n)), - ).toStrictEqual(new NixInt(3n)); - expect( - getBuiltin("add").apply(new NixFloat(1)).apply(new NixFloat(2)), - ).toStrictEqual(new NixFloat(3)); -}); - -test("'builtins.add' throws when trying to add two strings", () => { - expect(() => - getBuiltin("add").apply(new NixString("a")).apply(new NixString("b")), - ).toThrow( - new EvalException("value is of type 'string' while a number was expected."), - ); - expect(() => - getBuiltin("add").apply(new NixFloat(1)).apply(new NixString("b")), - ).toThrow( - new EvalException("value is of type 'string' while a number was expected."), - ); -}); diff --git a/nixjs-rt/src/builtins/tests/all.test.ts b/nixjs-rt/src/builtins/tests/all.test.ts deleted file mode 100644 index 7312ca4..0000000 --- a/nixjs-rt/src/builtins/tests/all.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.all' on lists", () => { - expect( - getBuiltin("all") - .apply(new Lambda((ctx) => ctx)) - .apply(new NixList([n.TRUE, n.TRUE, n.TRUE])), - ).toBe(n.TRUE); - expect( - getBuiltin("all") - .apply(new Lambda((ctx) => ctx)) - .apply(new NixList([n.TRUE, n.FALSE, n.TRUE])), - ).toBe(n.FALSE); -}); - -test("'builtins.all' on non-function throws", () => { - expect(() => getBuiltin("all").apply(new NixFloat(1))).toThrow( - new EvalException( - "'all' function requires another function, but got 'float' instead.", - ), - ); -}); - -test("'builtins.all' on non-list throws", () => { - expect(() => - getBuiltin("all") - .apply(new Lambda((ctx) => ctx)) - .apply(n.TRUE), - ).toThrow(new EvalException("Cannot apply the 'all' function on 'bool'.")); -}); diff --git a/nixjs-rt/src/builtins/tests/any.test.ts b/nixjs-rt/src/builtins/tests/any.test.ts deleted file mode 100644 index b79df44..0000000 --- a/nixjs-rt/src/builtins/tests/any.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.any' on lists", () => { - expect( - getBuiltin("any") - .apply(new Lambda((ctx) => ctx)) - .apply(new NixList([n.FALSE, n.FALSE, n.TRUE])), - ).toBe(n.TRUE); - expect( - getBuiltin("any") - .apply(new Lambda((ctx) => ctx)) - .apply(new NixList([n.FALSE, n.FALSE, n.FALSE])), - ).toBe(n.FALSE); -}); - -test("'builtins.any' on non-function throws", () => { - expect(() => getBuiltin("any").apply(new NixFloat(1))).toThrow( - new EvalException( - "'any' function requires another function, but got 'float' instead.", - ), - ); -}); - -test("'builtins.any' on non-list throws", () => { - expect(() => - getBuiltin("any") - .apply(new Lambda((ctx) => ctx)) - .apply(n.TRUE), - ).toThrow(new EvalException("Cannot apply the 'any' function on 'bool'.")); -}); diff --git a/nixjs-rt/src/builtins/tests/attrNames.test.ts b/nixjs-rt/src/builtins/tests/attrNames.test.ts deleted file mode 100644 index d359254..0000000 --- a/nixjs-rt/src/builtins/tests/attrNames.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.attrNames' on sets", () => { - expect( - getBuiltin("attrNames") - .apply( - new StrictAttrset( - new Map([ - ["b", n.FALSE], - ["a", n.TRUE], - ["c", new NixFloat(1)], - ]), - ), - ) - .toJs(), - ).toStrictEqual(["a", "b", "c"]); -}); - -test("'builtins.attrNames' on non-sets throws", () => { - expect(() => getBuiltin("attrNames").apply(n.TRUE)).toThrow( - new EvalException("Cannot apply the 'attrNames' function on 'bool'."), - ); -}); diff --git a/nixjs-rt/src/builtins/tests/attrValues.test.ts b/nixjs-rt/src/builtins/tests/attrValues.test.ts deleted file mode 100644 index 9e9f741..0000000 --- a/nixjs-rt/src/builtins/tests/attrValues.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.attrValues' on sets", () => { - expect( - getBuiltin("attrValues") - .apply( - new StrictAttrset( - new Map([ - ["b", n.FALSE], - ["a", n.TRUE], - ["c", new NixFloat(1)], - ]), - ), - ) - .toJs(), - ).toStrictEqual([true, false, 1]); -}); - -test("'builtins.attrValues' on non-sets throws", () => { - expect(() => getBuiltin("attrValues").apply(n.TRUE)).toThrow( - new EvalException("Cannot apply the 'attrValues' function on 'bool'."), - ); -}); diff --git a/nixjs-rt/src/builtins/tests/head.test.ts b/nixjs-rt/src/builtins/tests/head.test.ts deleted file mode 100644 index 7ee02b5..0000000 --- a/nixjs-rt/src/builtins/tests/head.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { beforeEach, expect, test } from "@jest/globals"; -import * as n from "../../lib"; -import { - Attrset, - attrset, - AttrsetBody, - EMPTY_ATTRSET, - EvalCtx, - EvalException, - Lambda, - Lazy, - NixFloat, - NixInt, - NixList, - NixString, - NixType, - Path, - StrictAttrset, -} from "../../lib"; -import { evalCtx, getBuiltin } from "../../testUtils"; - -test("'builtins.head' on lists", () => { - expect( - getBuiltin("head").apply(new NixList([new NixFloat(1), new NixFloat(2)])), - ).toStrictEqual(new NixFloat(1)); -}); - -test("'builtins.head' throws when list is empty", () => { - expect(() => getBuiltin("head").apply(new NixList([]))).toThrow( - new EvalException("Cannot fetch the first element in an empty list."), - ); -}); - -test("'builtins.head' on non-lists throws", () => { - expect(() => getBuiltin("head").apply(new NixFloat(1))).toThrow( - new EvalException("Cannot apply the 'head' function on 'float'."), - ); -}); diff --git a/nixjs-rt/src/errors/abort.ts b/nixjs-rt/src/errors/abort.ts new file mode 100644 index 0000000..975e674 --- /dev/null +++ b/nixjs-rt/src/errors/abort.ts @@ -0,0 +1,15 @@ +import { ErrorMessage, err, NixError, instanceToClass } from "."; +import { NixTypeClass, NixTypeInstance } from "../lib"; + +export class NixAbortError { + constructor(public readonly message: string) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`Aborted: '${this.message}'`; + } +} + +export function abortError(message: string) { + let abort = new NixAbortError(message); + return new NixError(abort, abort.toDefaultErrorMessage()); +} diff --git a/nixjs-rt/src/errors/attribute.ts b/nixjs-rt/src/errors/attribute.ts new file mode 100644 index 0000000..3c896a9 --- /dev/null +++ b/nixjs-rt/src/errors/attribute.ts @@ -0,0 +1,28 @@ +import { ErrorMessage, err, NixError, instanceToClass, highlighted } from "."; +import { NixTypeClass, NixTypeInstance } from "../lib"; + +export class NixAttributeAlreadyDefinedError { + constructor(public readonly attrPath: string[]) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`Attribute '${highlighted(this.attrPath.join("."))}' is already defined'`; + } +} + +export function attributeAlreadyDefinedError(attrPath: string[]) { + let error = new NixAttributeAlreadyDefinedError(attrPath); + return new NixError(error, error.toDefaultErrorMessage()); +} + +export class NixMissingAttributeError { + constructor(public readonly attrPath: string[]) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`Attribute '${highlighted(this.attrPath.join("."))}' is missing'`; + } +} + +export function missingAttributeError(attrPath: string[]) { + let error = new NixMissingAttributeError(attrPath); + return new NixError(error, error.toDefaultErrorMessage()); +} diff --git a/nixjs-rt/src/errors/errorMessage.ts b/nixjs-rt/src/errors/errorMessage.ts new file mode 100644 index 0000000..32e0f98 --- /dev/null +++ b/nixjs-rt/src/errors/errorMessage.ts @@ -0,0 +1,160 @@ +import { + Attrset, + Lambda, + Lazy, + NixBool, + NixFloat, + NixInt, + NixList, + NixNull, + NixString, + NixTypeClass, + NixTypeInstance, + Path, +} from "../lib"; + +type PlainErrorMessagePart = { + kind: "plain"; + value: string; +}; + +type HighlightedErrorMessagePart = { + kind: "highlighted"; + value: string; +}; + +type ErrorMessagePart = PlainErrorMessagePart | HighlightedErrorMessagePart; +export type ErrorMessage = ErrorMessagePart[]; + +/** A hack-y way of finding whether an object is an ErrorMessagePart. Essential for error message building. */ +function isErrorMessagePart(part: any): part is ErrorMessagePart { + return ( + typeof part === "object" && + part !== null && + "kind" in part && + typeof part.kind === "string" + ); +} + +function isErrorMessage(parts: any): parts is ErrorMessage { + if (!Array.isArray(parts)) { + return false; + } + + return parts.every(isErrorMessagePart); +} + +/** A helper to convert instances to their respective class in a type-safe way, for better error messages */ +export function instanceToClass(instance: NixTypeInstance | NixTypeClass) { + if (instance instanceof NixBool) { + return NixBool; + } else if (instance instanceof NixFloat) { + return NixFloat; + } else if (instance instanceof NixInt) { + return NixInt; + } else if (instance instanceof NixList) { + return NixList; + } else if (instance instanceof NixNull) { + return NixNull; + } else if (instance instanceof NixString) { + return NixString; + } else if (instance instanceof Path) { + return Path; + } else if (instance instanceof Lambda) { + return Lambda; + } else if (instance instanceof Attrset) { + return Attrset; + } else if (instance instanceof Lazy) { + return instanceToClass(instance.toStrict()); + } else { + return instance; + } +} + +export function stringifyErrorMessage(message: ErrorMessage): string { + return message.map((part) => part.value).join(""); +} + +type ErrorMessageBuilderPart = + | string + // | NixTypeClass + // | NixTypeClass[] + // | NixTypeInstance + | ErrorMessage; + +function builderPartToErrMessage(part: ErrorMessageBuilderPart): ErrorMessage { + if (typeof part === "string") { + return [{ kind: "plain", value: part }]; + } else { + return part; + } +} + +/** + * Tag function for building error messages, especially type mismatch messages. + * + * # Example: + * ```ts + * err`Expected ${errTypes(NixInt, NixString)}, but got ${errType(value)}` + * ``` + */ +export function err( + strings: readonly string[], + ...parts: readonly ErrorMessageBuilderPart[] +): ErrorMessage { + // Join the strings and parts together + const messageParts: ErrorMessagePart[] = []; + for (let i = 0; i < strings.length; i++) { + messageParts.push(...builderPartToErrMessage(strings[i])); + if (i < parts.length) { + messageParts.push(...builderPartToErrMessage(parts[i])); + } + } + + return messageParts; +} + +/** + * Generates a highlighted human-readable representation of a single type + * + * E.g. `NixInt` becomes `a number` + */ +export function errType(type: NixTypeClass | NixTypeInstance): ErrorMessage { + return [ + { kind: "highlighted", value: instanceToClass(type).toHumanReadable() }, + ]; +} + +/** + * Generates a highlighted human-readable representation of multiple types + * + * E.g. `[NixInt, NixFloat, NixString]` becomes `a number, a float, or a string` + */ +export function errTypes(...types: NixTypeClass[]): ErrorMessage { + if (types.length === 0) { + throw new Error("errTypes: types array is empty"); + } + + if (types.length === 1) { + return errType(types[0]); + } + + // Dynamically build the error message, separating with commas and "or" + const message: ErrorMessage = []; + for (let i = 0; i < types.length; i++) { + if (i === types.length - 1) { + message.push(...err`or`); + } else if (i > 0) { + message.push(...err`,`); + } + message.push(...errType(types[i])); + } +} + +export function highlighted(message: string | ErrorMessage): ErrorMessage { + const msg = err`${message}`; + return msg.map((part) => ({ + kind: "highlighted", + value: part.value, + })); +} diff --git a/nixjs-rt/src/errors/function.ts b/nixjs-rt/src/errors/function.ts new file mode 100644 index 0000000..1c09ec2 --- /dev/null +++ b/nixjs-rt/src/errors/function.ts @@ -0,0 +1,15 @@ +import { ErrorMessage, err, NixError, instanceToClass, highlighted } from "."; +import { NixTypeClass, NixTypeInstance } from "../lib"; + +export class NixFunctionCallWithoutArgumentError { + constructor(public readonly argument: string) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`Function call is missing required argument '${highlighted(this.argument)}'`; + } +} + +export function functionCallWithoutArgumentError(argument: string) { + let error = new NixFunctionCallWithoutArgumentError(argument); + return new NixError(error, error.toDefaultErrorMessage()); +} diff --git a/nixjs-rt/src/errors/index.ts b/nixjs-rt/src/errors/index.ts new file mode 100644 index 0000000..9b869dc --- /dev/null +++ b/nixjs-rt/src/errors/index.ts @@ -0,0 +1,49 @@ +import { + Attrset, + Lambda, + Lazy, + NixBool, + NixFloat, + NixInt, + NixList, + NixNull, + NixString, + NixType, + NixTypeClass, + NixTypeInstance, + Path, +} from "../lib"; +import { NixAbortError } from "./abort"; +import { + NixAttributeAlreadyDefinedError, + NixMissingAttributeError, +} from "./attribute"; +import { ErrorMessage } from "./errorMessage"; +import { NixFunctionCallWithoutArgumentError } from "./function"; +import { NixOtherError } from "./other"; +import { NixTypeMismatchError } from "./typeError"; +import { NixCouldntFindVariableError } from "./variable"; + +export * from "./errorMessage"; + +type NixErrorKind = + | NixTypeMismatchError + | NixAbortError + | NixOtherError + | NixMissingAttributeError + | NixAttributeAlreadyDefinedError + | NixFunctionCallWithoutArgumentError + | NixCouldntFindVariableError; + +/** The base error class. This class gets parsed in rix by Rust code. */ +export class NixError extends Error { + constructor( + public readonly kind: NixErrorKind, + public readonly richMessage: ErrorMessage, + ) { + // TODO: In the future, make error messages have color highlighting for special error parts + const messageString = richMessage.map((part) => part.value).join(""); + + super(messageString); + } +} diff --git a/nixjs-rt/src/errors/other.ts b/nixjs-rt/src/errors/other.ts new file mode 100644 index 0000000..8691a31 --- /dev/null +++ b/nixjs-rt/src/errors/other.ts @@ -0,0 +1,15 @@ +import { ErrorMessage, err, NixError, instanceToClass } from "."; +import { NixTypeClass, NixTypeInstance } from "../lib"; + +export class NixOtherError { + constructor(public readonly message: string) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`${this.message}`; + } +} + +export function otherError(message: string) { + let other = new NixOtherError(message); + return new NixError(other, other.toDefaultErrorMessage()); +} diff --git a/nixjs-rt/src/errors/typeError.ts b/nixjs-rt/src/errors/typeError.ts new file mode 100644 index 0000000..8afd8fd --- /dev/null +++ b/nixjs-rt/src/errors/typeError.ts @@ -0,0 +1,42 @@ +import { + ErrorMessage, + err, + NixError, + instanceToClass, + errType, + errTypes, +} from "."; +import { NixTypeClass, NixTypeInstance } from "../lib"; + +export class NixTypeMismatchError { + constructor( + public readonly expected: NixTypeClass[], + public readonly got: NixTypeClass, + ) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`Expected ${errTypes(...this.expected)}, but got ${errType(this.got)}`; + } +} + +export function typeMismatchError( + got: NixTypeClass | NixTypeInstance, + expected: NixTypeClass | NixTypeClass[], + message?: ErrorMessage, +) { + if (!Array.isArray(expected)) { + expected = [expected]; + } + + const error = new NixTypeMismatchError(expected, instanceToClass(got)); + return new NixError(error, message ?? error.toDefaultErrorMessage()); +} + +/** Similar to a type mismatch error, but with expected being [] and the message is required */ +export function invalidTypeError( + got: NixTypeClass | NixTypeInstance, + message: ErrorMessage, +) { + const error = new NixTypeMismatchError([], instanceToClass(got)); + return new NixError(error, message); +} diff --git a/nixjs-rt/src/errors/variable.ts b/nixjs-rt/src/errors/variable.ts new file mode 100644 index 0000000..4940531 --- /dev/null +++ b/nixjs-rt/src/errors/variable.ts @@ -0,0 +1,15 @@ +import { ErrorMessage, err, NixError, instanceToClass, highlighted } from "."; +import { NixTypeClass, NixTypeInstance } from "../lib"; + +export class NixCouldntFindVariableError { + constructor(public readonly varName: string) {} + + toDefaultErrorMessage(): ErrorMessage { + return err`Couldn't find variable '${highlighted(this.varName)}'`; + } +} + +export function couldntFindVariableError(varName: string) { + let error = new NixCouldntFindVariableError(varName); + return new NixError(error, error.toDefaultErrorMessage()); +} diff --git a/nixjs-rt/src/lib.test.ts b/nixjs-rt/src/lib.test.ts index 38ab79f..a6b9add 100644 --- a/nixjs-rt/src/lib.test.ts +++ b/nixjs-rt/src/lib.test.ts @@ -26,14 +26,6 @@ test("calling a lambda should return its value", () => { ); }); -test("calling something that isn't a lambda should throw", () => { - expect(() => new NixInt(1n).apply(EMPTY_ATTRSET)).toThrow( - new EvalException( - "Attempt to call something which is not a function but is 'int'.", - ), - ); -}); - // Arithmetic: test("unary '-' operator on integers", () => { expect(new NixInt(1n).neg()).toStrictEqual(new NixInt(-1n)); @@ -43,10 +35,6 @@ test("unary '-' operator on floats", () => { expect(new NixFloat(2.5).neg()).toStrictEqual(new NixFloat(-2.5)); }); -test("unary '-' operator on non-numbers", () => { - expect(() => new NixString("a").neg()).toThrow(n.EvalException); -}); - test("'+' operator on integers", () => { expect((new NixInt(1n).add(new NixInt(2n)) as NixInt).number).toBe(3); expect( @@ -67,17 +55,6 @@ test("'+' operator on mixed integers and floats", () => { expect(new NixFloat(2.0).add(new NixInt(1n)).toJs()).toBe(3.0); }); -test("'+' operator on mixed numbers and non-numbers", () => { - expect(() => new NixString("a").add(new NixInt(1n))).toThrow(n.EvalException); - expect(() => new NixInt(1n).add(new NixString("a"))).toThrow(n.EvalException); - expect(() => new NixFloat(1).add(new NixString("a"))).toThrow( - n.EvalException, - ); - expect(() => new NixString("a").add(new NixFloat(1))).toThrow( - n.EvalException, - ); -}); - test("'+' operator on strings", () => { expect(new NixString("a").add(new NixString("b")).toJs()).toBe("ab"); }); @@ -112,15 +89,6 @@ test("'-' operator on mixed integers and floats", () => { expect(new NixFloat(2.0).sub(new NixInt(1n)).toJs()).toBe(1); }); -test("'-' operator on non-numbers raises exceptions", () => { - expect(() => new NixString("foo").sub(new NixFloat(1))).toThrow( - n.EvalException, - ); - expect(() => new NixFloat(1).sub(new NixString("foo"))).toThrow( - n.EvalException, - ); -}); - test("'*' operator on integers", () => { const result = new NixInt(2n).mul(new NixInt(3n)) as NixInt; expect(result.number).toBe(6); @@ -137,18 +105,6 @@ test("'*' operator on mixed integers and floats", () => { expect(new NixFloat(3.5).mul(new NixInt(2n))).toStrictEqual(new NixFloat(7)); }); -test("'*' operator on non-numbers raises exceptions", () => { - expect(() => new NixString("foo").mul(new NixString("bar"))).toThrow( - n.EvalException, - ); - expect(() => new NixString("foo").mul(new NixFloat(1))).toThrow( - n.EvalException, - ); - expect(() => new NixString("foo").mul(new NixInt(1n))).toThrow( - n.EvalException, - ); -}); - test("'/' operator on integers", () => { expect(new NixInt(5n).div(new NixInt(2n))).toStrictEqual(new NixInt(2n)); }); @@ -168,18 +124,6 @@ test("'/' operator on mixed integers and floats", () => { ); }); -test("'/' operator on non-numbers raises exceptions", () => { - expect(() => new NixString("foo").div(new NixString("bar"))).toThrow( - n.EvalException, - ); - expect(() => new NixString("foo").div(new NixFloat(1))).toThrow( - n.EvalException, - ); - expect(() => new NixString("foo").div(new NixInt(1n))).toThrow( - n.EvalException, - ); -}); - // Attrset: test("attrset construction", () => { expect(attrset(evalCtx(), keyVals()).toJs()).toStrictEqual(new Map()); @@ -223,42 +167,6 @@ test("attrsets ignore null attrs", () => { ).toStrictEqual(new Map([["a", new Map()]])); }); -test("attrset construction with repeated attrs throws", () => { - expect(() => - attrset( - evalCtx(), - keyVals(["a", new NixFloat(1)], ["a", new NixFloat(1)]), - ).toJs(), - ).toThrow(new EvalException("Attribute 'a' already defined.")); - expect(() => - attrset( - evalCtx(), - keyVals(["a", new NixFloat(1)], ["a.b", new NixFloat(2)]), - ).toJs(), - ).toThrow(new EvalException("Attribute 'a' already defined.")); - expect(() => - attrset( - evalCtx(), - keyVals(["a.b", new NixFloat(1)], ["a.b", new NixFloat(2)]), - ).toJs(), - ).toThrow(new EvalException("Attribute 'a.b' already defined.")); - expect(() => - attrset( - evalCtx(), - keyVals( - ["a.b", attrset(evalCtx(), keyVals(["c", new NixFloat(1)]))], - ["a.b.c", new NixFloat(2)], - ), - ).toJs(), - ).toThrow(new EvalException("Attribute 'a.b.c' already defined.")); -}); - -test("attrset with non-string attrs throw", () => { - expect(() => - attrset(evalCtx(), (_) => [[[new NixFloat(1)], new NixFloat(1)]]).toJs(), - ).toThrow(n.EvalException); -}); - test("'//' operator on attrsets", () => { expect( attrset(evalCtx(), keyVals()).update(attrset(evalCtx(), keyVals())).toJs(), @@ -285,15 +193,6 @@ test("'//' operator on attrsets", () => { ).toStrictEqual(new Map([["a", 2]])); }); -test("'//' operator on non-attrsets raises exceptions", () => { - expect(() => attrset(evalCtx(), keyVals()).update(new NixFloat(1))).toThrow( - n.EvalException, - ); - expect(() => new NixFloat(1).update(attrset(evalCtx(), keyVals()))).toThrow( - n.EvalException, - ); -}); - test("'?' operator", () => { expect(attrset(evalCtx(), keyVals()).has([new NixString("a")])).toBe(n.FALSE); expect( @@ -351,12 +250,6 @@ test("'.' operator", () => { ).toBe(5); }); -test("'.' operator throws when attrpath doesn't exist", () => { - expect(() => - attrset(evalCtx(), keyVals()).select([new NixString("a")], undefined), - ).toThrow(n.EvalException); -}); - test("recursive attrsets allow referencing attributes defined later", () => { expect( n @@ -404,57 +297,26 @@ test("recursive attrsets allow referencing attributes from other attribute names ); }); -test("non-recursive attrsets don't allow references to other attributes in the attrset", () => { - expect(() => - n - .attrset(evalCtx(), (ctx) => [ - [toAttrpath("a"), ctx.lookup("b").add(new NixFloat(1))], - [toAttrpath("b"), new NixFloat(1)], - ]) - .select([new NixString("a")], undefined) - .toJs(), - ).toThrow(n.EvalException); -}); - // Boolean: test("'&&' operator on booleans", () => { expect(n.TRUE.and(n.FALSE)).toBe(n.FALSE); expect(n.FALSE.and(new NixFloat(1))).toBe(n.FALSE); // emulates nix's behaviour }); -test("'&&' operator on non-booleans raises exceptions", () => { - expect(() => n.TRUE.and(new NixFloat(1))).toThrow(n.EvalException); - expect(() => new NixFloat(1).and(n.TRUE)).toThrow(n.EvalException); -}); - test("'->' operator on booleans", () => { expect(n.FALSE.implication(n.FALSE)).toBe(n.TRUE); expect(n.FALSE.implication(new NixFloat(1))).toBe(n.TRUE); // emulates nix's behaviour }); -test("'->' operator on non-booleans raises exceptions", () => { - expect(() => n.TRUE.implication(new NixFloat(1))).toThrow(n.EvalException); - expect(() => new NixFloat(1).implication(n.TRUE)).toThrow(n.EvalException); -}); - test("'!' operator on booleans", () => { expect(n.FALSE.invert()).toBe(n.TRUE); }); -test("'!' operator on non-booleans raises exceptions", () => { - expect(() => new NixFloat(1).invert()).toThrow(n.EvalException); -}); - test("'||' operator on booleans", () => { expect(n.TRUE.or(n.FALSE).toJs()).toBe(true); expect(n.TRUE.or(new NixFloat(1)).toJs()).toBe(true); // emulates nix's behaviour }); -test("'||' operator on non-booleans raises exceptions", () => { - expect(() => n.FALSE.or(new NixFloat(1))).toThrow(n.EvalException); - expect(() => new NixFloat(1).or(n.TRUE)).toThrow(n.EvalException); -}); - // Comparison: test("'==' operator on numbers", () => { expect(new NixFloat(1).eq(new NixFloat(2))).toBe(n.FALSE); @@ -536,25 +398,11 @@ test("'<' operator on numbers", () => { expect(new NixFloat(1).less(new NixInt(2n))).toBe(n.TRUE); }); -test("'<' operator on mixed-types throws", () => { - expect(() => new NixInt(1n).less(n.TRUE)).toThrow(n.EvalException); - expect(() => n.TRUE.less(new NixInt(1n))).toThrow(n.EvalException); - expect(() => n.TRUE.less(new NixFloat(1))).toThrow(n.EvalException); -}); - test("'<' operator on strings", () => { expect(new NixString("a").less(new NixString("b"))).toBe(n.TRUE); expect(new NixString("foo").less(new NixString("b"))).toBe(n.FALSE); }); -test("'<' operator on booleans throws", () => { - expect(() => n.FALSE.less(n.TRUE)).toThrow(n.EvalException); -}); - -test("'<' operator on null values throws", () => { - expect(() => n.NULL.less(n.NULL)).toThrow(n.EvalException); -}); - test("'<' operator on lists", () => { expect(new NixList([]).less(new NixList([]))).toBe(n.FALSE); expect(new NixList([]).less(new NixList([new NixFloat(1)]))).toBe(n.TRUE); @@ -612,20 +460,6 @@ test("'<' operator on lists with lazy values", () => { ).toBe(n.FALSE); }); -test("'<' operator list invalid", () => { - expect(() => - new NixList([n.TRUE]).less(new NixList([new NixFloat(1)])), - ).toThrow(n.EvalException); - expect(() => new NixList([n.TRUE]).less(new NixList([n.FALSE]))).toThrow( - n.EvalException, - ); -}); - -test("'<' operator on attrsets invalid", () => { - let smallAttrset = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); - expect(() => smallAttrset.less(smallAttrset)).toThrow(n.EvalException); -}); - test("'<' operator on paths", () => { expect(new Path("./a").less(new Path("./b"))).toStrictEqual(n.TRUE); expect(new Path("./a").less(new Path("./a"))).toStrictEqual(n.FALSE); @@ -689,19 +523,6 @@ test("pattern lambda with default values", () => { ).toBe(1); }); -test("pattern lambda with missing parameter", () => { - let innerCtx = evalCtx().withShadowingScope( - n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])), - ); - expect(() => - n - .patternLambda(innerCtx, undefined, [["a", undefined]], (evalCtx) => - evalCtx.lookup("a"), - ) - .apply(n.attrset(evalCtx(), keyVals())), - ).toThrow(n.EvalException); -}); - test("pattern lambda with arguments binding", () => { const arg = n.attrset(evalCtx(), keyVals(["a", new NixFloat(1)])); expect( @@ -765,14 +586,6 @@ test("'++' operator on lazy lists with lazy values", () => { .toJs(), ).toStrictEqual([1, 2]); }); - -test("'++' operator on non-lists raises exceptions", () => { - expect(() => new NixList([]).concat(new NixFloat(1))).toThrow( - n.EvalException, - ); - expect(() => n.TRUE.concat(new NixList([]))).toThrow(n.EvalException); -}); - // Path: test("toPath on absolute paths", () => { expect(n.toPath(evalCtx(), "/a")).toStrictEqual(new Path("/a")); @@ -785,11 +598,6 @@ test("toPath transforms relative paths with 'joinPaths'", () => { expect(n.toPath(evalCtx(), "./a")).toStrictEqual(new Path("/test_base/a")); }); -// Scope: -test("variable not in global scope", () => { - expect(() => evalCtx().lookup("foo")).toThrow(n.EvalException); -}); - test("variable in shadowing scope", () => { expect( evalCtx() diff --git a/nixjs-rt/src/lib.ts b/nixjs-rt/src/lib.ts index 6da5956..e4eee86 100644 --- a/nixjs-rt/src/lib.ts +++ b/nixjs-rt/src/lib.ts @@ -1,4 +1,37 @@ -import { getBuiltins } from "./builtins/builtins"; +import { getBuiltins } from "./builtins"; +import { NixError, err, errType } from "./errors"; +import { + NixFunctionCallWithoutArgumentError, + functionCallWithoutArgumentError, +} from "./errors/function"; +import { + NixAttributeAlreadyDefinedError, + NixMissingAttributeError, + missingAttributeError, +} from "./errors/attribute"; +import { NixOtherError, otherError } from "./errors/other"; +import { + NixTypeMismatchError, + invalidTypeError, + typeMismatchError, +} from "./errors/typeError"; +import { + NixCouldntFindVariableError, + couldntFindVariableError, +} from "./errors/variable"; +import { NixAbortError } from "./errors/abort"; + +// Error re-exports +export { NixError } from "./errors"; +export { NixFunctionCallWithoutArgumentError } from "./errors/function"; +export { + NixAttributeAlreadyDefinedError, + NixMissingAttributeError, +} from "./errors/attribute"; +export { NixOtherError } from "./errors/other"; +export { NixTypeMismatchError } from "./errors/typeError"; +export { NixCouldntFindVariableError } from "./errors/variable"; +export { NixAbortError } from "./errors/abort"; // Types: export class EvalException extends Error { @@ -89,7 +122,7 @@ export class EvalCtx implements Scope { return value; } } - throw new EvalException(`Could not find variable '${name}'.`); + throw couldntFindVariableError(name); } } @@ -98,8 +131,9 @@ export abstract class NixType { * This method implements the `+` operator. It adds the `rhs` value to this value. */ add(rhs: NixType): NixType { - throw new EvalException( - `Cannot add '${this.typeOf()}' to '${rhs.typeOf()}'.`, + throw invalidTypeError( + this, + err`Cannot add ${errType(rhs)} to ${errType(this)}`, ); } @@ -108,32 +142,31 @@ export abstract class NixType { } apply(param: NixType): NixType { - throw new EvalException( - `Attempt to call something which is not a function but is '${this.typeOf()}'.`, + throw invalidTypeError( + this, + err`Attempt to call something which is not a function but is ${errType(this)}`, ); } asBoolean(): boolean { - throw new EvalException( - `Value is '${this.typeOf()}' but a boolean was expected.`, - ); + throw typeMismatchError(this, NixBool); } asString(): string { - throw new EvalException( - `Value is '${this.typeOf()}' but a string was expected.`, - ); + throw typeMismatchError(this, NixString); } concat(other: NixType): NixList { - throw new EvalException( - `Cannot concatenate '${this.typeOf()}' and '${other.typeOf()}'.`, + throw invalidTypeError( + this, + err`Cannot concatenate ${errType(this)} and ${errType(other)}`, ); } div(rhs: NixType): NixInt | NixFloat { - throw new EvalException( - `Cannot divide '${this.typeOf()}' and '${rhs.typeOf()}'.`, + throw invalidTypeError( + this, + err`Cannot divide ${errType(this)} with ${errType(rhs)}`, ); } @@ -160,8 +193,9 @@ export abstract class NixType { * This method implements the `<` operator. It checks whether the `rhs` value is lower than this value. */ less(rhs: NixType): NixBool { - throw new EvalException( - `Cannot compare '${this.typeOf()}' with '${rhs.typeOf()}'; values of that type are incomparable`, + throw invalidTypeError( + this, + err`Cannot compare ${errType(this)} with ${errType(rhs)}`, ); } @@ -178,13 +212,14 @@ export abstract class NixType { } mul(rhs: NixType): NixInt | NixFloat { - throw new EvalException( - `Cannot multiply '${this.typeOf()}' and '${rhs.typeOf()}'.`, + throw invalidTypeError( + this, + err`Cannot multiply ${errType(this)} with ${errType(rhs)}`, ); } neg(): NixInt | NixFloat { - throw new EvalException(`Cannot negate '${this.typeOf()}'.`); + throw invalidTypeError(this, err`Cannot negate ${errType(this)}`); } neq(rhs: NixType): NixBool { @@ -196,15 +231,19 @@ export abstract class NixType { } select(attrPath: NixType[], defaultValue: NixType | undefined): NixType { - throw new EvalException(`Cannot select attribute from '${this.typeOf()}'.`); + throw invalidTypeError( + this, + err`Cannot select attribute from ${errType(this)}`, + ); } /** * This method implements the `-` operator. It subtracts the `rhs` value from this value. */ sub(rhs: NixType): NixInt | NixFloat { - throw new EvalException( - `Cannot subtract '${this.typeOf()}' and '${rhs.typeOf()}'.`, + throw invalidTypeError( + this, + err`Cannot subtract ${errType(rhs)} from ${errType(this)}`, ); } @@ -227,14 +266,35 @@ export abstract class NixType { */ abstract typeOf(): string; + /** + * Returns a human-readable string representation of this value, that can be inserted into a sentence. + * + * For example, "a string", "an array", etc. + * + * Static functions can't be made abstract, so abstract is omitted here. + */ + static toHumanReadable(): string { + throw new Error("abstract"); + } + + /** + * Returns the name of the type (as a string, which effectively acts as an enum). + * + * This is used for identifying types for error messages. + */ + static toTypeName(): NixTypeName { + throw new Error("abstract"); + } + /** * Returns a new attrset whose attributes are a union of this attrset and the right-hand-side attrset. * The values are taken from the right-hand-side attrset or from this attrset. Values from the * right-hand-side attrset override values from this attrset. */ update(rhs: NixType): Attrset { - throw new EvalException( - `Cannot merge '${this.typeOf()}' with '${rhs.typeOf()}'. Can only merge attrset with attrset.`, + throw invalidTypeError( + this, + err`Cannot merge ${errType(this)} with ${errType(rhs)}`, ); } } @@ -255,6 +315,14 @@ export class NixBool extends NixType { return "bool"; } + static toHumanReadable(): string { + return "a boolean"; + } + + static toTypeName(): NixTypeName { + return "bool"; + } + toJs(): boolean { return this.value; } @@ -295,8 +363,10 @@ export abstract class Attrset extends NixType implements Scope { get(attrName: NixType): undefined | NixType { attrName = attrName.toStrict(); if (!(attrName instanceof NixString)) { - throw new EvalException( - `Attribute name must be a string but '${attrName.typeOf()}' given.`, + throw typeMismatchError( + attrName, + NixString, + err`Attribute name must be ${errType(NixString)}, but got ${errType(attrName)}`, ); } return this.lookup(attrName.value); @@ -358,11 +428,7 @@ export abstract class Attrset extends NixType implements Scope { if (value === undefined) { if (defaultValue === undefined) { - throw new EvalException( - `Attribute '${attrPath - .map((attrName) => attrName.asString()) - .join(".")}' is missing.`, - ); + throw missingAttributeError(attrPath.map((attr) => attr.asString())); } return defaultValue; } @@ -381,6 +447,14 @@ export abstract class Attrset extends NixType implements Scope { return "set"; } + static toHumanReadable(): string { + return "a set"; + } + + static toTypeName(): NixTypeName { + return "set"; + } + /** * Returns a copy of this attrset as a strict (fully-evaluated) JavaScript Map. */ @@ -459,7 +533,7 @@ class AttrsetBuilder implements Scope { const currentEntryIdx = this.pendingEntryIdx++; const [attrPath, value] = this.entries[currentEntryIdx]; if (attrPath.length === 0) { - throw new EvalException( + throw otherError( "Cannot add an undefined attribute name to the attrset.", ); } @@ -533,8 +607,15 @@ function _recursiveDisjointMerge( function _assertIsMergeable(value: NixType, attrPath: string[]): Attrset { const valueStrict = value.toStrict(); if (!(valueStrict instanceof Attrset)) { - throw new EvalException( - `Attribute '${attrPath.join(".")}' already defined.`, + // FIXME: Is this error relevant? I'm not sure why it was here, but I'll leave it as a comment + // throw new EvalException( + // `Attribute '${attrPath.join(".")}' already defined.`, + // ); + + throw typeMismatchError( + valueStrict, + Attrset, + err`Cannot merge ${errType(valueStrict)} with ${errType(Attrset)}`, ); } return valueStrict; @@ -643,6 +724,14 @@ export class NixFloat extends NixType { typeOf(): string { return "float"; } + + static toHumanReadable(): string { + return "a float"; + } + + static toTypeName(): NixTypeName { + return "float"; + } } export class NixInt extends NixType { @@ -739,6 +828,14 @@ export class NixInt extends NixType { typeOf(): string { return "int"; } + + static toHumanReadable(): string { + return "an int"; + } + + static toTypeName(): NixTypeName { + return "int"; + } } export class NixList extends NixType { @@ -810,6 +907,14 @@ export class NixList extends NixType { typeOf(): string { return "list"; } + + static toHumanReadable(): string { + return "a list"; + } + + static toTypeName(): NixTypeName { + return "list"; + } } export class NixNull extends NixType { @@ -824,6 +929,14 @@ export class NixNull extends NixType { typeOf(): string { return "null"; } + + static toHumanReadable(): string { + return "a null"; + } + + static toTypeName(): NixTypeName { + return "null"; + } } export const NULL = new NixNull(); @@ -873,6 +986,14 @@ export class NixString extends NixType { typeOf(): string { return "string"; } + + static toHumanReadable(): string { + return "a string"; + } + + static toTypeName(): NixTypeName { + return "string"; + } } export class Path extends NixType { @@ -909,6 +1030,14 @@ export class Path extends NixType { typeOf(): string { return "path"; } + + static toHumanReadable(): string { + return "a path"; + } + + static toTypeName(): NixTypeName { + return "path"; + } } export class Lazy extends NixType { @@ -1031,8 +1160,14 @@ export class Lazy extends NixType { return this.toStrict().typeOf(); } - override update(rhs: NixType): Attrset { - return this.toStrict().update(rhs); + static toHumanReadable(): string { + // This static method should never be called + throw new Error("Lazy value isn't a real type"); + } + + static toTypeName(): NixTypeName { + // This static method should never be called + throw new Error("Lazy value isn't a real type"); } } @@ -1055,6 +1190,14 @@ export class Lambda extends NixType { typeOf(): string { return "lambda"; } + + static toHumanReadable(): string { + return "a lambda"; + } + + static toTypeName(): NixTypeName { + return "lambda"; + } } // Attrset: @@ -1104,9 +1247,7 @@ export function patternLambda( let paramValue = param.lookup(paramName); if (paramValue === undefined) { if (defaultValue === undefined) { - throw new EvalException( - `Function called without required argument '${paramName}'.`, - ); + throw functionCallWithoutArgumentError(paramName); } paramValue = defaultValue; } @@ -1187,7 +1328,7 @@ function _attrPathToValue( value: NixType, ): undefined | NixType { if (attrPath.length === 0) { - throw new EvalException("Unexpected attr path of zero length."); + throw otherError("Unexpected attr path of zero length."); } let attrName = attrPath[0].toStrict(); @@ -1237,3 +1378,30 @@ export function withExpr( ): any { return body(evalCtx.withNonShadowingScope(namespace)); } + +export const allNixTypeClasses = [ + NixBool, + NixFloat, + NixInt, + NixList, + NixNull, + NixString, + Path, + Lazy, + Lambda, + Attrset, +]; + +export type NixTypeName = + | "bool" + | "float" + | "int" + | "list" + | "null" + | "string" + | "path" + | "lambda" + | "set"; + +export type NixTypeClass = (typeof allNixTypeClasses)[number]; +export type NixTypeInstance = InstanceType; diff --git a/src/cmd/eval.rs b/src/cmd/eval.rs index 2085f71..950dcc3 100644 --- a/src/cmd/eval.rs +++ b/src/cmd/eval.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use crate::cmd::{to_cmd_err, RixSubCommand}; +use crate::eval::error::NixError; use crate::eval::execution; use crate::eval::types::Value; use clap::{Arg, ArgAction, ArgMatches}; @@ -23,7 +24,7 @@ pub fn cmd() -> RixSubCommand { } } -pub fn handle_cmd(parsed_args: &ArgMatches) -> Result<(), String> { +pub fn handle_cmd(parsed_args: &ArgMatches) -> Result<(), NixError> { let expr = parsed_args .get_one::("expr") .ok_or("You must use the '--expr' option. Nothing else is implemented :)")?; diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 210938b..ae42145 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -4,21 +4,23 @@ use clap::{ArgMatches, Command}; use colored::*; use std::process::ExitCode; +use crate::eval::error::NixError; + pub struct RixSubCommand { pub name: &'static str, pub cmd: fn(Command) -> Command, pub handler: fn(&ArgMatches) -> Result<(), ExitCode>, } -pub fn print_err(msg: &str) { +pub fn print_err(msg: NixError) { eprintln!("{}: {}", "error".red(), msg); } -pub fn to_cmd_err(result: Result<(), String>) -> Result<(), ExitCode> { - result.map_err(|err| print_and_err(&err)) +pub fn to_cmd_err(result: Result<(), NixError>) -> Result<(), ExitCode> { + result.map_err(print_and_err) } -pub fn print_and_err(msg: &str) -> ExitCode { +pub fn print_and_err(msg: NixError) -> ExitCode { print_err(msg); ExitCode::FAILURE } diff --git a/src/cmd/transpile.rs b/src/cmd/transpile.rs index f6d7588..2ae5c96 100644 --- a/src/cmd/transpile.rs +++ b/src/cmd/transpile.rs @@ -1,5 +1,6 @@ use crate::cmd::{to_cmd_err, RixSubCommand}; use crate::eval::emit_js; +use crate::eval::error::NixError; use clap::{Arg, ArgAction, ArgMatches}; pub fn cmd() -> RixSubCommand { @@ -20,7 +21,7 @@ pub fn cmd() -> RixSubCommand { } } -pub fn handle_cmd(parsed_args: &ArgMatches) -> Result<(), String> { +pub fn handle_cmd(parsed_args: &ArgMatches) -> Result<(), NixError> { let expression = parsed_args .get_one::("EXPRESSION") .ok_or("You must provide a single expression to transpile.")?; diff --git a/src/eval/error.rs b/src/eval/error.rs new file mode 100644 index 0000000..be5fdd0 --- /dev/null +++ b/src/eval/error.rs @@ -0,0 +1,313 @@ +use super::helpers::{call_js_function, get_js_value_key, is_nixrt_type}; + +#[derive(Debug)] +pub struct NixError { + pub message: Vec, + pub kind: NixErrorKind, +} + +impl std::fmt::Display for NixError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for part in &self.message { + match part { + NixErrorMessagePart::Plain(text) => write!(f, "{}", text)?, + NixErrorMessagePart::Highlighted(text) => write!(f, "{}", text)?, + } + } + + Ok(()) + } +} + +impl From for NixError { + fn from(message: String) -> Self { + NixError { + message: vec![NixErrorMessagePart::Plain(message.clone())], + kind: NixErrorKind::UnexpectedRustError { message }, + } + } +} + +impl<'a> From<&'a str> for NixError { + fn from(message: &'a str) -> Self { + NixError { + message: vec![NixErrorMessagePart::Plain(message.to_string())], + kind: NixErrorKind::UnexpectedRustError { + message: message.to_string(), + }, + } + } +} + +impl From for NixError { + fn from(error: v8::DataError) -> Self { + NixError { + message: vec![NixErrorMessagePart::Plain(error.to_string())], + kind: NixErrorKind::UnexpectedJsError { + message: error.to_string(), + }, + } + } +} + +#[derive(Debug)] +pub enum NixErrorMessagePart { + Plain(String), + Highlighted(String), +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum NixTypeKind { + Bool, + Float, + Int, + List, + Null, + String, + Path, + Lambda, + Set, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum NixErrorKind { + Abort { + message: String, + }, + CouldntFindVariable { + var_name: String, + }, + TypeMismatch { + expected: Vec, + got: NixTypeKind, + }, + Other { + message: String, + }, + MissingAttribute { + attr_path: Vec, + }, + AttributeAlreadyDefined { + attr_path: Vec, + }, + FunctionCallWithoutArgument { + argument: String, + }, + + // For non-nix errors thrown in js or rust + UnexpectedJsError { + message: String, + }, + UnexpectedRustError { + message: String, + }, +} + +pub fn js_error_to_rust( + scope: &mut v8::HandleScope, + nixrt: v8::Local, + error: v8::Local, +) -> NixError { + let result = try_js_error_to_rust(scope, nixrt, error); + + match result { + Ok(ok) => ok, + Err(err) => err, + } +} + +fn try_js_error_to_rust( + scope: &mut v8::HandleScope, + nixrt: v8::Local, + error: v8::Local, +) -> Result { + // If the error is not a NixError instance, then it's an unexpected error. + if !is_nixrt_type(scope, &nixrt, &error, "NixError")? { + return Ok(NixError { + message: vec![NixErrorMessagePart::Plain("An error occurred.".to_owned())], + kind: NixErrorKind::UnexpectedJsError { + message: error.to_rust_string_lossy(scope), + }, + }); + } + + let message_js = get_js_value_key(scope, &error, "richMessage")?; + let message = js_error_message_to_rust(scope, message_js)?; + + let kind_js = get_js_value_key(scope, &error, "kind")?; + + let kind = if is_nixrt_type(scope, &nixrt, &kind_js, "NixAbortError")? { + let message_js = get_js_value_key(scope, &kind_js, "message")?; + let message = message_js.to_rust_string_lossy(scope); + NixErrorKind::Abort { message } + } else if is_nixrt_type(scope, &nixrt, &kind_js, "NixCouldntFindVariableError")? { + let var_name_js = get_js_value_key(scope, &kind_js, "varName")?; + let var_name = var_name_js.to_rust_string_lossy(scope); + NixErrorKind::CouldntFindVariable { var_name } + } else if is_nixrt_type(scope, &nixrt, &kind_js, "NixTypeMismatchError")? { + let expected_js = get_js_value_key(scope, &kind_js, "expected")?; + let got_js = get_js_value_key(scope, &kind_js, "got")?; + + let mut expected = nix_type_class_array_to_enum_array(scope, nixrt, expected_js)?; + let got = nix_type_class_to_enum(scope, nixrt, got_js)?; + + // Sort expected array, for normalization + expected.sort_unstable(); + + NixErrorKind::TypeMismatch { expected, got } + } else if is_nixrt_type(scope, &nixrt, &kind_js, "NixOtherError")? { + let message_js = get_js_value_key(scope, &kind_js, "message")?; + let message = message_js.to_rust_string_lossy(scope); + NixErrorKind::Other { message } + } else if is_nixrt_type(scope, &nixrt, &kind_js, "NixMissingAttributeError")? { + let attr_path_js = get_js_value_key(scope, &kind_js, "attrPath")?; + let attr_path = js_string_array_to_rust_string_array(scope, attr_path_js)?; + NixErrorKind::MissingAttribute { attr_path } + } else if is_nixrt_type(scope, &nixrt, &kind_js, "NixAttributeAlreadyDefinedError")? { + let attr_path_js = get_js_value_key(scope, &kind_js, "attrPath")?; + let attr_path = js_string_array_to_rust_string_array(scope, attr_path_js)?; + NixErrorKind::AttributeAlreadyDefined { attr_path } + } else if is_nixrt_type( + scope, + &nixrt, + &kind_js, + "NixFunctionCallWithoutArgumentError", + )? { + let argument_js = get_js_value_key(scope, &kind_js, "argument")?; + let argument = argument_js.to_rust_string_lossy(scope); + NixErrorKind::FunctionCallWithoutArgument { argument } + } else { + return Ok(NixError { + message: vec![NixErrorMessagePart::Plain("An error occurred.".to_owned())], + kind: NixErrorKind::UnexpectedJsError { + message: error.to_rust_string_lossy(scope), + }, + }); + }; + + Ok(NixError { message, kind }) +} + +fn nix_type_class_to_enum( + scope: &mut v8::HandleScope, + nixrt: v8::Local, + class: v8::Local, +) -> Result { + let name_fn = get_js_value_key(scope, &class, "toTypeName")?.try_into()?; + let name_js_str = call_js_function(scope, &name_fn, nixrt, &[])?; + let name = name_js_str.to_rust_string_lossy(scope); + + match name.as_str() { + "bool" => Ok(NixTypeKind::Bool), + "float" => Ok(NixTypeKind::Float), + "int" => Ok(NixTypeKind::Int), + "list" => Ok(NixTypeKind::List), + "null" => Ok(NixTypeKind::Null), + "string" => Ok(NixTypeKind::String), + "path" => Ok(NixTypeKind::Path), + "lambda" => Ok(NixTypeKind::Lambda), + "set" => Ok(NixTypeKind::Set), + _ => Err(NixError { + message: vec![NixErrorMessagePart::Plain(format!( + "Unexpected type name: {name}" + ))], + kind: NixErrorKind::Other { + message: format!("Unexpected type name: {name}"), + }, + }), + } +} + +fn nix_type_class_array_to_enum_array( + scope: &mut v8::HandleScope, + nixrt: v8::Local, + class_array: v8::Local, +) -> Result, NixError> { + let class_array: v8::Local = class_array.try_into()?; + + let len_num: v8::Local = + get_js_value_key(scope, &class_array, "length")?.try_into()?; + let len = len_num.value() as u32; + + let mut result = Vec::with_capacity(len as usize); + + for i in 0..len { + let item_class = class_array + .get_index(scope, i) + .ok_or_else(|| format!("Expected index {i} not found."))?; + + let kind = nix_type_class_to_enum(scope, nixrt, item_class)?; + result.push(kind); + } + + Ok(result) +} + +fn js_string_array_to_rust_string_array( + scope: &mut v8::HandleScope, + js_array: v8::Local, +) -> Result, NixError> { + let js_array: v8::Local = js_array.try_into()?; + + let len_num: v8::Local = + get_js_value_key(scope, &js_array, "length")?.try_into()?; + let len = len_num.value() as u32; + + let mut result = Vec::with_capacity(len as usize); + + for i in 0..len { + let item_js = js_array + .get_index(scope, i) + .ok_or_else(|| format!("Expected index {i} not found."))?; + + let item = item_js.to_rust_string_lossy(scope); + result.push(item); + } + + Ok(result) +} + +fn js_error_message_part_to_rust( + scope: &mut v8::HandleScope, + error_part: v8::Local, +) -> Result { + let kind_js = get_js_value_key(scope, &error_part, "kind")?; + let kind = kind_js.to_rust_string_lossy(scope); + + match kind.as_str() { + "plain" => { + let value_js = get_js_value_key(scope, &error_part, "value")?; + let value = value_js.to_rust_string_lossy(scope); + Ok(NixErrorMessagePart::Plain(value)) + } + "highlighted" => { + let value_js = get_js_value_key(scope, &error_part, "value")?; + let value = value_js.to_rust_string_lossy(scope); + Ok(NixErrorMessagePart::Highlighted(value)) + } + _ => Err("Unexpected error message part kind.".into()), + } +} + +fn js_error_message_to_rust( + scope: &mut v8::HandleScope, + error: v8::Local, +) -> Result, NixError> { + let error: v8::Local = error.try_into()?; + + let len_num: v8::Local = get_js_value_key(scope, &error, "length")?.try_into()?; + let len = len_num.value() as u32; + + let mut result = Vec::with_capacity(len as usize); + + for i in 0..len { + let error_part = error + .get_index(scope, i) + .ok_or_else(|| format!("Expected index {i} not found."))?; + + let part = js_error_message_part_to_rust(scope, error_part)?; + result.push(part); + } + + Ok(result) +} diff --git a/src/eval/execution.rs b/src/eval/execution.rs index 0c063d8..fe182e6 100644 --- a/src/eval/execution.rs +++ b/src/eval/execution.rs @@ -7,7 +7,8 @@ use v8::{HandleScope, Local, ModuleStatus, Object}; use crate::eval::types::EvalResult; use super::emit_js::emit_module; -use super::helpers::{get_nixrt_type, try_get_js_object_key}; +use super::error::NixError; +use super::helpers::{call_js_function, get_nixrt_type, try_get_js_object_key}; use super::types::js_value_to_nix; static INIT_V8: Once = Once::new(); @@ -29,7 +30,7 @@ pub fn evaluate(nix_expr: &str) -> EvalResult { // Execute the Nix runtime JS module, get its exports let nixjs_rt_str = include_str!("../../nixjs-rt/dist/lib.mjs"); - let nixjs_rt_obj = exec_module(nixjs_rt_str, scope).unwrap(); + let nixjs_rt_obj = exec_module(nixjs_rt_str, scope)?; // Set them to a global variable let nixrt_attr = v8::String::new(scope, "n").unwrap(); @@ -45,10 +46,11 @@ pub fn evaluate(nix_expr: &str) -> EvalResult { fn nix_expr_to_js_function<'s>( scope: &mut HandleScope<'s>, nix_expr: &str, -) -> Result, String> { +) -> Result, NixError> { let source_str = emit_module(nix_expr)?; let module_source_v8 = to_v8_source(scope, &source_str, ""); - let module = v8::script_compiler::compile_module(scope, module_source_v8).unwrap(); + let module = v8::script_compiler::compile_module(scope, module_source_v8) + .ok_or("Failed to compile the module.")?; if module .instantiate_module(scope, resolve_module_callback) @@ -68,7 +70,10 @@ fn nix_expr_to_js_function<'s>( todo!("evaluation failed:\n{}", string); } - let namespace_obj = module.get_module_namespace().to_object(scope).unwrap(); + let namespace_obj = module + .get_module_namespace() + .to_object(scope) + .ok_or("Failed to get the module namespace.")?; let Some(nix_value) = try_get_js_object_key(scope, &namespace_obj.into(), "default")? else { todo!( @@ -95,7 +100,7 @@ fn import_nix_module<'s>( let nix_fn = match nix_fn { Ok(nix_fn) => nix_fn, Err(err) => { - let err_str = v8::String::new(scope, &err).unwrap(); + let err_str = v8::String::new(scope, &err.to_string()).unwrap(); let err_obj = v8::Exception::error(scope, err_str); ret.set(err_obj); return; @@ -108,22 +113,26 @@ fn import_nix_module<'s>( fn exec_module<'a>( code: &str, scope: &mut v8::HandleScope<'a>, -) -> Result, String> { +) -> Result, NixError> { let source = to_v8_source(scope, code, ""); - let module = v8::script_compiler::compile_module(scope, source).unwrap(); + let module = v8::script_compiler::compile_module(scope, source) + .ok_or("Failed to compile the module.")?; if module .instantiate_module(scope, resolve_module_callback) .is_none() { - return Err("Instantiation failure.".to_owned()); + return Err("Instantiation failure.".to_owned().into()); } if module.evaluate(scope).is_none() { - return Err("Evaluation failure.".to_owned()); + return Err("Evaluation failure.".to_owned().into()); } - let obj = module.get_module_namespace().to_object(scope).unwrap(); + let obj = module + .get_module_namespace() + .to_object(scope) + .ok_or("Failed to get the module namespace.")?; Ok(obj) } @@ -151,37 +160,18 @@ fn nix_value_from_module( })?, )?; - let nix_value = call_js_function(scope, &nix_value, &[eval_ctx.into()])?; + let nix_value = call_js_function(scope, &nix_value, nixjs_rt_obj, &[eval_ctx.into()])?; let to_strict_fn: v8::Local = try_get_js_object_key(scope, &nixrt, "recursiveStrict")? .expect("Could not find the function `recursiveStrict` in `nixrt`.") .try_into() .expect("`n.recursiveStrict` is not a function."); - let strict_nix_value = call_js_function(scope, &to_strict_fn, &[nix_value])?; + let strict_nix_value = call_js_function(scope, &to_strict_fn, nixjs_rt_obj, &[nix_value])?; js_value_to_nix(scope, &nixrt, &strict_nix_value) } -fn call_js_function<'s>( - scope: &mut v8::ContextScope<'_, v8::HandleScope<'s>>, - js_function: &v8::Local, - args: &[v8::Local], -) -> Result, String> { - let scope = &mut v8::TryCatch::new(scope); - let recv = v8::undefined(scope).into(); - let Some(strict_nix_value) = js_function.call(scope, recv, args) else { - // TODO: Again, the stack trace needs to be source-mapped. See TODO above. - let err_msg = scope - .stack_trace() - .map_or("Unknown evaluation error.".to_owned(), |stack| { - stack.to_rust_string_lossy(scope) - }); - return Err(err_msg); - }; - Ok(strict_nix_value) -} - fn create_eval_ctx<'s>( scope: &mut v8::HandleScope<'s>, nixrt: &v8::Local, diff --git a/src/eval/helpers.rs b/src/eval/helpers.rs index 619aec7..9b980b8 100644 --- a/src/eval/helpers.rs +++ b/src/eval/helpers.rs @@ -1,10 +1,15 @@ -pub fn is_nixrt_type( +use super::error::{js_error_to_rust, NixError}; + +pub fn is_nixrt_type<'s, T>( scope: &mut v8::HandleScope<'_>, - nixrt: &v8::Local, + nixrt: &v8::Local<'s, T>, js_value: &v8::Local, type_name: &str, -) -> Result { - let nixrt_type = get_nixrt_type(scope, nixrt, type_name)?; +) -> Result +where + v8::Local<'s, T>: Into>, +{ + let nixrt_type = get_nixrt_type(scope, &(*nixrt).into(), type_name)?; js_value.instance_of(scope, nixrt_type).ok_or_else(|| { format!( "Failed to check whether value '{}' is '{type_name}'.", @@ -39,3 +44,46 @@ pub fn try_get_js_object_key<'s>( let key_js_str = v8::String::new(scope, key).unwrap(); Ok(js_object.get(scope, key_js_str.into())) } + +pub fn get_js_value_key<'s: 'v, 'v, T>( + scope: &mut v8::HandleScope<'s>, + js_value: &v8::Local<'v, T>, + key: &str, +) -> Result, String> +where + v8::Local<'v, T>: TryInto>, +{ + let js_object: v8::Local<'v, v8::Object> = (*js_value) + .try_into() + .map_err(|_| "Not an object.".to_owned())?; + let key_js_str = v8::String::new(scope, key).unwrap(); + + if let Some(value) = js_object.get(scope, key_js_str.into()) { + Ok(value) + } else { + Err(format!("Expected key '{key}' not found.")) + } +} + +pub fn call_js_function<'s>( + scope: &mut v8::HandleScope<'s>, + js_function: &v8::Local, + nixrt: v8::Local, + args: &[v8::Local], +) -> Result, NixError> { + let try_scope = &mut v8::TryCatch::new(scope); + let recv = v8::undefined(try_scope).into(); + let Some(strict_nix_value) = js_function.call(try_scope, recv, args) else { + // TODO: Again, the stack trace needs to be source-mapped. + if let Some(error) = try_scope.exception() { + let error = js_error_to_rust(try_scope, nixrt, error); + + dbg!(&error); + + return Err(error); + } else { + return Err("Unknown evaluation error.".into()); + } + }; + Ok(strict_nix_value) +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 407d72f..f521a4b 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -1,4 +1,5 @@ pub mod emit_js; +pub mod error; pub mod execution; pub mod helpers; pub mod tests; diff --git a/src/eval/tests.rs b/src/eval/tests.rs index 6ce31f8..ec87567 100644 --- a/src/eval/tests.rs +++ b/src/eval/tests.rs @@ -2,7 +2,7 @@ mod tests { use std::collections::HashMap; - use crate::eval::types::Value; + use crate::eval::{error::NixErrorKind, types::Value}; use super::super::execution::*; @@ -330,10 +330,12 @@ mod tests { #[test] fn test_eval_builtin_abort() { let error_msg = evaluate(r#"abort "foo""#).unwrap_err(); - let expected_msg = "Evaluation aborted with the following error message: 'foo'"; - assert!( - error_msg.contains(expected_msg), - "Error message '{error_msg}' didn't contain '{expected_msg}'." + let expected_msg = "foo"; + assert_eq!( + error_msg.kind, + NixErrorKind::Abort { + message: expected_msg.to_owned() + } ); } diff --git a/src/eval/types.rs b/src/eval/types.rs index 89816c6..2242223 100644 --- a/src/eval/types.rs +++ b/src/eval/types.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -use super::helpers::{is_nixrt_type, try_get_js_object_key}; +use super::{ + error::NixError, + helpers::{is_nixrt_type, try_get_js_object_key}, +}; #[derive(Debug, PartialEq)] pub enum Value { @@ -14,7 +17,7 @@ pub enum Value { Str(String), } -pub type EvalResult = Result; +pub type EvalResult = Result; pub fn js_value_to_nix( scope: &mut v8::HandleScope<'_>, @@ -61,7 +64,7 @@ fn from_js_int( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "NixInt")? { let Some(int64_js_value) = try_get_js_object_key(scope, js_value, "int64")? else { return Ok(None); @@ -78,7 +81,7 @@ fn from_js_string( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "NixString")? { let Some(value) = try_get_js_object_key(scope, js_value, "value")? else { return Ok(None); @@ -97,7 +100,7 @@ fn from_js_lazy( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "Lazy")? { let to_strict = try_get_js_object_key(scope, js_value, "toStrict")?.ok_or_else(|| { "Internal error: could not find the `toStrict` method on the Lazy object.".to_string() @@ -119,7 +122,7 @@ fn from_js_bool( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "NixBool")? { let value = try_get_js_object_key(scope, js_value, "value")?.ok_or_else(|| { "Internal error: could not find the `value` property on the NixBool object.".to_string() @@ -138,7 +141,7 @@ fn from_js_float( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "NixFloat")? { let value = try_get_js_object_key(scope, js_value, "value")?.ok_or_else(|| { "Internal error: could not find the `value` property on the NixFloat object." @@ -162,7 +165,7 @@ fn from_js_attrset( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "Attrset")? { let underlying_map_value = try_get_js_object_key(scope, js_value, "underlyingMap")? .ok_or_else(|| { @@ -192,7 +195,7 @@ fn from_js_list( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if is_nixrt_type(scope, nixrt, js_value, "NixList")? { let value = try_get_js_object_key(scope, js_value, "values")?.ok_or_else(|| { "Internal error: could not find the `values` property on the NixList object." @@ -212,7 +215,7 @@ fn from_js_path( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if !is_nixrt_type(scope, nixrt, js_value, "Path")? { return Ok(None); } @@ -226,7 +229,7 @@ fn from_js_lambda( scope: &mut v8::HandleScope<'_>, nixrt: &v8::Local, js_value: &v8::Local, -) -> Result, String> { +) -> Result, NixError> { if !is_nixrt_type(scope, nixrt, js_value, "Lambda")? { return Ok(None); } diff --git a/src/main.rs b/src/main.rs index 17e932b..544a137 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,5 +23,5 @@ fn dispatch_cmd(parsed_args: &clap::ArgMatches, subcommands: &[&cmd::RixSubComma .map_or_else(|err| err, |_| ExitCode::SUCCESS); } } - cmd::print_and_err("operation not supported") + cmd::print_and_err("operation not supported".into()) }