Skip to content
This repository has been archived by the owner on Jul 3, 2024. It is now read-only.

Commit

Permalink
✅[#1] Passes all tests
Browse files Browse the repository at this point in the history
- Flag some error messages as TODO
- Add variadic "if" (which is a chain of elifs)
- Add "?:"
- Handle more "var" error cases
  • Loading branch information
CharString committed Nov 13, 2023
1 parent c33e3a2 commit 2ceb8e5
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 64 deletions.
86 changes: 78 additions & 8 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const f = (mu1: MonoType, mu2: MonoType, ...rest: MonoType[]): TypeFunctionAppli
const [mu3, ...extra] = rest;
return {type: 'ty-app', C: '->', mus: [mu1, f(mu2, mu3, ...extra)]};
};
const a: TypeVariable = {type: 'ty-var', a: 'a'};
const b: TypeVariable = {type: 'ty-var', a: 'b'};
// const c: TypeVariable = {type: 'ty-var', a: 'c'};
const makeTypeVars = (n: number): TypeVariable[] =>
[...'abcdefghijklmnopqrstuvwxyz'].slice(0, n).map(a => ({type: 'ty-var', a}));
const [a, b] = makeTypeVars(2);
const forall = (typevars: TypeVariable[], sigma: PolyType): TypeQuantifier => {
const [{a: name}, ...rest] = typevars;
return {type: 'ty-quantifier', a: name, sigma: rest.length ? forall(rest, sigma) : sigma};
Expand All @@ -47,6 +47,7 @@ export const defaultContext = makeContext({
missing_some: f(number, array(string), array(string)),
// Logic and Boolean Operations
if: forall([a, b], f(a, b, b, b)),
'?:': forall([a, b], f(a, b, b, b)), // ternary from tests.json
// TODO: should the parameters of (in-)equaility be of the same type 🤔
// forcing === and !== isn't a eslint rule for nothing...
'==': forall([a, b], f(a, b, bool)),
Expand Down Expand Up @@ -141,12 +142,30 @@ const maybeJsonLogicExpression = (json: JSONValue, context: Context): json is JS
*/
const repr = (thing: unknown): string => JSON.stringify(thing) || thing?.toString?.() || '';

/**
* @thing - any JSONValue
* @return a string for display the type of `thing` to the user
*/
const reprType = (thing: JSONValue, context: Context): string => {
if (typeof thing === 'boolean') return 'Boolean';
if (thing === null) return 'Null';
if (typeof thing === 'number') return 'Number';
if (typeof thing === 'string') return 'String';
if (Array.isArray(thing)) return 'Array';
try {
if (thing && maybeJsonLogicExpression(thing, context)) {
return 'JsonLogic rule';
}
} catch (error) {}
return 'Object';
};

/**
* Turn exp `{"var": "foo"}` into `["var", ["foo"]]`
*/
const destructureJsonLogicExpression = (
exp: Readonly<JSONObject>,
context: Readonly<Context>,
context: Readonly<Context>
): [operation: string, params: JSONArray] => {
const [operation, ...tooMany] = operationsFrom(exp, context);
if (tooMany.length) {
Expand Down Expand Up @@ -287,11 +306,24 @@ export const parseJsonLogicExpression = (
if (operation === 'var') {
const [varPath, defaultValue, ...tooMany] = args;
// parseErrors
if (varPath === undefined)
throw Error(
`The "var" operation needs a string or positive integer argument.\n` +
`It's missing in ${repr(json)}. Did you mean {"var": [""]} ?`
);
if (!isString(varPath) && !isNatural(varPath))
throw Error(`The argument of "var" operation should be a string or positive integer
I found a ${typeof varPath} in: ${repr(json)}.`);
throw Error(
`The argument of a "var" operation should be a string or positive integer.\n` +
`I found a ${reprType(varPath, context)} in: ${repr(json)}.${
!varPath
? '\nDid you mean {"var": [""]} ?'
: maybeJsonLogicExpression(varPath, context)
? '\nIt could be correct; "var" can take a rule that describes a string or positive integer value. But I can\'t judge the correctness of its further use, because that completely depends on the data.'
: ''
}`
);
if (tooMany.length) {
throw Error(`JsonLogicExpression may only contain one operation.
throw Error(`The "var" operation takes only one value.
I found ${repr(args)} in: ${repr(json)}.
Maybe something went wrong with your braces?`);
}
Expand All @@ -317,11 +349,49 @@ export const parseJsonLogicExpression = (
operation = `3-ary ${operation}`;
} else if (new Set(['-', '+']).has(operation) && args.length === 1) {
operation = `1-ary ${operation}`;
} else if ((operation === '+' || operation === '*') && args.length > 2) {
} else if ((operation === '+' || operation === '*') && args.length != 2) {
// NB unary + should be handled already...
// monomorphise sum and product versions of + and *
operation = `${args.length}-ary ${operation}`;
// add n-ary function to the context as f(n+1 numbers) (one extra for the return value)
context[operation] = f(number, number, ...Array(args.length - 1).fill(number));
} else if ((operation === 'and' || operation === 'or') && args.length != 2) {
if (args.length === 1)
throw Error(
`This ${operation} operation is incomplete ${repr(json)}.\n` +
`Either add more arguments or replace it with just ${args[0]}.`
);
// variadic and/or
// monomorphise to: forall a b c ... .:: a -> b -> c -> ... -> bool
operation = `${args.length}-ary ${operation}`;
const [a, b, ...cdef] = makeTypeVars(args.length);
context[operation] = forall([a, b, ...cdef], f(a, b, ...cdef, bool));
} else if (operation === 'if') {
if (args.length % 2 == 0 || args.length == 1) {
throw Error(
`This ${operation} operation is incomplete ${repr(json)}.${
args.length >= 2
? // TODO: add a variable infer its type, (int/string) and suggest resp. 0 and ""
'\n"var" takes an odd number of values. Did you forget the value for the else case?'
: ''
}`
);
}
if (args.length > 3) {
// it's easy to make mistakes in long "elif chains"
// let's enforce explicit bools instead of truthy values
operation = `${args.length}-ary ${operation}`;
// bool, a, bool, a, ..., a, a
context[operation] = f(
bool,
a,
...Array((args.length - 3) / 2)
.fill(null)
.flatMap(_ => [bool, a]),
a,
a
);
}
} else if (new Set(['map', 'filter', 'all', 'some', 'none']).has(operation)) {
const [newContext, [arrayExp, e2]] = parseValues(args, context);
return [
Expand Down
18 changes: 17 additions & 1 deletion test-d/jsonlogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import {W} from '../src/w';
import cases from './tests.json';

type Case = [rule: JSONValue, data: JSONValue, result: JSONValue];
// type ErrorCase = [rule: JSONValue, data: JSONValue, result: JSONValue, error: string];
// obvious cast; imported json
// filter "# commment string"
const isCase = (testCase: string | unknown[]): testCase is Case =>
Array.isArray(testCase) && testCase.length == 3;
type ErrorCase = [rule: JSONValue, data: JSONValue, result: JSONValue, error: string];

const isErrorCase = (testCase: string | unknown[]): testCase is ErrorCase =>
Array.isArray(testCase) && testCase.length == 4;

const inferResultType = (jsonLogic: JSONValue, data: JSONValue): string => {
const context = parseContext(data);
Expand Down Expand Up @@ -39,4 +42,17 @@ describe('Test against JsonLogic suite', () => {
expect(t).toBe(getType(result));
}
);
it.each(cases.filter(isErrorCase))(
'can detect %j, with context %j, does not typecheck with %j and raises some Error containing %j',
(rule: JSONValue, data: JSONValue, result: JSONValue, error: string) => {
if (error.startsWith("TODO"))
expect(() => inferResultType(rule, data)).toThrow("")
else if (error.startsWith("Valid")){
const t = inferResultType(rule, data);
expect(t).not.toBe(getType(result));
} else
expect(() => inferResultType(rule, data)).toThrow(error)

}
);
});
Loading

0 comments on commit 2ceb8e5

Please sign in to comment.