Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ add(a, b) = a + b
factorial(x) = x < 2 ? 1 : x * factorial(x - 1)
```

These functions can than be used in other functions that require a function argument, such as `map`, `filter` or `fold`:

```js
name(u) = u.name; map(name, users)
add(a, b) = a+b; fold(add, 0, [1, 2, 3])
```

You can also define the functions inline:

```js
filter(isEven(x) = x % 2 == 0, [1, 2, 3, 4, 5])
```

## Custom JavaScript Functions

If you need additional functions that aren't supported out of the box, you can easily add them in your own code. Instances of the `Parser` class have a property called `functions` that's simply an object with all the functions that are in scope. You can add, replace, or delete any of the properties to customize what's available in the expressions. For example:
Expand Down
6 changes: 3 additions & 3 deletions src/core/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* It uses a stack-based interpreter to evaluate instruction sequences produced by the parser.
*/

import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, IWHENMATCH, ICASEELSE, ICASECOND, IWHENCOND, IOBJECT, IPROPERTY, IOBJECTEND } from '../parsing/instruction.js';
import { ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IEXPREVAL, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, IWHENMATCH, ICASEELSE, ICASECOND, IWHENCOND, IOBJECT, IPROPERTY, IOBJECTEND } from '../parsing/instruction.js';
import type { Instruction } from '../parsing/instruction.js';
import type { Expression } from './expression.js';
import type { Value, Values, VariableResolveResult, VariableAlias, VariableValue } from '../types/values.js';
import { VariableError } from '../types/errors.js';
import { ExpressionValidator } from '../validation/expression-validator.js';

// cSpell:words INUMBER IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY
// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY
// cSpell:words IFUNDEF IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY
// cSpell:words IOBJECT IOBJECTEND
// cSpell:words nstack
Expand Down Expand Up @@ -131,7 +131,7 @@ function evaluateExpressionToken(expr: Expression, values: EvaluationValues, tok
let operatorFunction: Function, functionArgs: any[], argumentCount: number;

const { type } = token;
if (type === INUMBER || type === IVARNAME) {
if (type === ISCALAR || type === IVARNAME) {
nstack.push(token.value);
} else if (type === IOP2) {
rightOperand = nstack.pop();
Expand Down
6 changes: 3 additions & 3 deletions src/core/expression-to-string.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// cSpell:words INUMBER IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY
// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY
// cSpell:words IFUNDEF IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY
// cSpell:words IOBJECT IOBJECTEND
// cSpell:words nstack

import { INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, ICASECOND, IWHENCOND, IWHENMATCH, ICASEELSE, IOBJECT, IOBJECTEND, IPROPERTY } from '../parsing/instruction.js';
import { ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IEXPR, IMEMBER, IENDSTATEMENT, IARRAY, IUNDEFINED, ICASEMATCH, ICASECOND, IWHENCOND, IWHENMATCH, ICASEELSE, IOBJECT, IOBJECTEND, IPROPERTY } from '../parsing/instruction.js';
import type { Instruction } from '../parsing/instruction.js';

export default function expressionToString(tokens: Instruction[], toJS?: boolean): string {
Expand All @@ -15,7 +15,7 @@ export default function expressionToString(tokens: Instruction[], toJS?: boolean
const item = tokens[i];
const { type } = item;

if (type === INUMBER) {
if (type === ISCALAR) {
if (typeof item.value === 'number' && item.value < 0) {
nstack.push('(' + item.value + ')');
} else if (Array.isArray(item.value)) {
Expand Down
16 changes: 8 additions & 8 deletions src/core/simplify.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Instruction, INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IEXPR, IMEMBER, IARRAY } from '../parsing/instruction.js';
import { Instruction, ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IEXPR, IMEMBER, IARRAY } from '../parsing/instruction.js';
import type { OperatorFunction } from '../types/parser.js';

export default function simplify(
Expand All @@ -17,10 +17,10 @@ export default function simplify(
const item = tokens[i];
const { type } = item;

if (type === INUMBER || type === IVARNAME) {
if (type === ISCALAR || type === IVARNAME) {
if (Array.isArray(item.value)) {
nstack.push(...simplify(
item.value.map((x) => new Instruction(INUMBER, x)).concat(new Instruction(IARRAY, item.value.length)),
item.value.map((x) => new Instruction(ISCALAR, x)).concat(new Instruction(IARRAY, item.value.length)),
unaryOps,
binaryOps,
ternaryOps,
Expand All @@ -30,13 +30,13 @@ export default function simplify(
nstack.push(item);
}
} else if (type === IVAR && Object.prototype.hasOwnProperty.call(values, item.value)) {
const newItem = new Instruction(INUMBER, values[item.value]);
const newItem = new Instruction(ISCALAR, values[item.value]);
nstack.push(newItem);
} else if (type === IOP2 && nstack.length > 1) {
n2 = nstack.pop()!;
n1 = nstack.pop()!;
f = binaryOps[item.value];
const newItem = new Instruction(INUMBER, f(n1.value, n2.value));
const newItem = new Instruction(ISCALAR, f(n1.value, n2.value));
nstack.push(newItem);
} else if (type === IOP3 && nstack.length > 2) {
n3 = nstack.pop()!;
Expand All @@ -46,13 +46,13 @@ export default function simplify(
nstack.push(n1.value ? n2.value : n3.value);
} else {
f = ternaryOps[item.value];
const newItem = new Instruction(INUMBER, f(n1.value, n2.value, n3.value));
const newItem = new Instruction(ISCALAR, f(n1.value, n2.value, n3.value));
nstack.push(newItem);
}
} else if (type === IOP1 && nstack.length > 0) {
n1 = nstack.pop()!;
f = unaryOps[item.value];
const newItem = new Instruction(INUMBER, f(n1.value));
const newItem = new Instruction(ISCALAR, f(n1.value));
nstack.push(newItem);
} else if (type === IEXPR) {
while (nstack.length > 0) {
Expand All @@ -61,7 +61,7 @@ export default function simplify(
newexpression.push(new Instruction(IEXPR, simplify(item.value as Instruction[], unaryOps, binaryOps, ternaryOps, values)));
} else if (type === IMEMBER && nstack.length > 0) {
n1 = nstack.pop()!;
nstack.push(new Instruction(INUMBER, n1.value[item.value]));
nstack.push(new Instruction(ISCALAR, n1.value[item.value]));
} else {
while (nstack.length > 0) {
newexpression.push(nstack.shift()!);
Expand Down
3 changes: 2 additions & 1 deletion src/language-service/language-service.documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,5 +224,6 @@ export const DEFAULT_CONSTANT_DOCS: Record<string, string> = {
E: 'Math.E',
PI: 'Math.PI',
true: 'Logical true',
false: 'Logical false'
false: 'Logical false',
null: 'Null value'
};
15 changes: 10 additions & 5 deletions src/language-service/language-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import {
TOP,
TNUMBER,
TCONST,
TSTRING,
TPAREN,
TBRACKET,
Expand Down Expand Up @@ -97,7 +98,9 @@ export function createLanguageService(options: LanguageServiceOptions | undefine
if (cachedConstants !== null) {
return cachedConstants;
}
cachedConstants = parser.consts ? Object.keys(parser.consts) : [];
cachedConstants = parser.numericConstants ? Object.keys(parser.numericConstants) : [];
cachedConstants = [...cachedConstants, ...Object.keys(parser.buildInLiterals)];

return cachedConstants;
}

Expand All @@ -107,6 +110,8 @@ export function createLanguageService(options: LanguageServiceOptions | undefine
return 'number';
case TSTRING:
return 'string';
case TCONST:
return 'constant';
case TKEYWORD:
return 'keyword';
case TOP:
Expand Down Expand Up @@ -144,7 +149,7 @@ export function createLanguageService(options: LanguageServiceOptions | undefine
return allConstants().map(name => ({
label: name,
kind: CompletionItemKind.Constant,
detail: valueTypeName(parser.consts[name]),
detail: valueTypeName(parser.numericConstants[name] ?? parser.buildInLiterals[name]),
documentation: constantDocs[name],
textEdit: { range: rangeFull, newText: name }
}));
Expand Down Expand Up @@ -237,12 +242,12 @@ export function createLanguageService(options: LanguageServiceOptions | undefine

// Constant hover
if (allConstants().includes(label)) {
const v = parser.consts[label];
const v = parser.numericConstants[label] ?? parser.buildInLiterals[label];
const doc = constantDocs[label];
const range: Range = {
start: textDocument.positionAt(span.start),
end: textDocument.positionAt(span.end)
};
}
return {
contents: {
kind: MarkupKind.PlainText,
Expand Down Expand Up @@ -270,7 +275,7 @@ export function createLanguageService(options: LanguageServiceOptions | undefine
}

// Numbers/strings
if (token.type === TNUMBER || token.type === TSTRING) {
if (token.type === TNUMBER || token.type === TSTRING || token.type === TCONST) {
const range: Range = { start: textDocument.positionAt(span.start), end: textDocument.positionAt(span.end) };
return { contents: { kind: MarkupKind.PlainText, value: `${valueTypeName(token.value)}` }, range };
}
Expand Down
2 changes: 1 addition & 1 deletion src/language-service/language-service.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface LanguageServiceApi {
}

export interface HighlightToken {
type: 'number' | 'string' | 'name' | 'keyword' | 'operator' | 'function' | 'punctuation';
type: 'number' | 'string' | 'name' | 'keyword' | 'operator' | 'function' | 'punctuation' | 'constant';
start: number;
end: number;
value?: string | number | boolean | undefined;
Expand Down
28 changes: 13 additions & 15 deletions src/parsing/instruction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// cSpell:words INUMBER IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY
// cSpell:words ISCALAR IVAR IVARNAME IFUNCALL IEXPR IEXPREVAL IMEMBER IENDSTATEMENT IARRAY
// cSpell:words IFUNDEF IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY
// cSpell:words IOBJECT IOBJECTEND

Expand All @@ -10,15 +10,15 @@
*
* Instruction type naming convention:
* - I = Instruction prefix
* - NUMBER = numeric literal
* - SCALAR = scalar literal
* - OP1/OP2/OP3 = unary/binary/ternary operators
* - VAR = variable reference
* - FUNCALL = function call
* - etc.
*/

/** Numeric literal instruction */
export const INUMBER = 'INUMBER' as const;
/** Scalar literal instruction */
export const ISCALAR = 'ISCALAR' as const;
/** Unary operator instruction (e.g., negation, factorial) */
export const IOP1 = 'IOP1' as const;
/** Binary operator instruction (e.g., +, -, *, /) */
Expand Down Expand Up @@ -66,7 +66,7 @@ export const IOBJECTEND = 'IOBJECTEND' as const;
* Union type for all instruction types
*/
export type InstructionType =
| typeof INUMBER
| typeof ISCALAR
| typeof IOP1
| typeof IOP2
| typeof IOP3
Expand All @@ -93,7 +93,7 @@ export type InstructionType =
* Discriminated union types for better type safety
*/
export interface NumberInstruction {
type: typeof INUMBER;
type: typeof ISCALAR;
value: number;
}

Expand Down Expand Up @@ -234,13 +234,7 @@ export class Instruction {

constructor(type: InstructionType, value?: any) {
this.type = type;
if (type === IUNDEFINED) {
this.value = undefined;
} else {
// this.value = (value !== undefined && value !== null) ? value : 0;
// We want to allow undefined values.
this.value = (value !== null) ? value : 0;
}
this.value = (type === IUNDEFINED) ? undefined : value;
}

/**
Expand All @@ -262,7 +256,7 @@ export class Instruction {

toString(): string {
switch (this.type) {
case INUMBER:
case ISCALAR:
case IOP1:
case IOP2:
case IOP3:
Expand Down Expand Up @@ -314,7 +308,11 @@ export function ternaryInstruction(value: string): Instruction {
}

export function numberInstruction(value: number): Instruction {
return new Instruction(INUMBER, value);
return new Instruction(ISCALAR, value);
}

export function scalarInstruction(value: boolean | null): Instruction {
return new Instruction(ISCALAR, value);
}

export function variableInstruction(value: string): Instruction {
Expand Down
17 changes: 9 additions & 8 deletions src/parsing/parser-state.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// cSpell:words TEOF TNUMBER TSTRING TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE
// cSpell:words INUMBER IVAR IFUNCALL IEXPREVAL IMEMBER IARRAY
// cSpell:words TEOF TNUMBER TSTRING TCONST TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE
// cSpell:words ISCALAR IVAR IFUNCALL IEXPREVAL IMEMBER IARRAY
// cSpell:words IUNDEFINED ICASEMATCH ICASECOND IWHENCOND IWHENMATCH ICASEELSE IPROPERTY
// cSpell:words IOBJECT IOBJECTEND

import { TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON, TEOF, TKEYWORD, TBRACE, Token, TokenType } from './token.js';
import { Instruction, INUMBER, IVAR, IFUNCALL, IMEMBER, IARRAY, IUNDEFINED, binaryInstruction, unaryInstruction, IWHENMATCH, ICASEMATCH, ICASEELSE, ICASECOND, IWHENCOND, IPROPERTY, IOBJECT, IOBJECTEND, InstructionType } from './instruction.js';
import {
TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON, TEOF, TKEYWORD, TBRACE, Token, TokenType,
TCONST
} from './token.js';
import { Instruction, ISCALAR, IVAR, IFUNCALL, IMEMBER, IARRAY, IUNDEFINED, binaryInstruction, unaryInstruction, IWHENMATCH, ICASEMATCH, ICASEELSE, ICASECOND, IWHENCOND, IPROPERTY, IOBJECT, IOBJECTEND, InstructionType } from './instruction.js';
import contains from '../core/contains.js';
import { TokenStream } from './token-stream.js';
import { ParseError, AccessError } from '../types/errors.js';
Expand Down Expand Up @@ -118,10 +121,8 @@ export class ParserState {
} else {
instr.push(new Instruction(IVAR, this.current!.value));
}
} else if (this.accept(TNUMBER)) {
instr.push(new Instruction(INUMBER, this.current!.value));
} else if (this.accept(TSTRING)) {
instr.push(new Instruction(INUMBER, this.current!.value));
} else if (this.accept(TNUMBER) ||this.accept(TSTRING) || this.accept(TCONST)) {
instr.push(new Instruction(ISCALAR, this.current!.value));
} else if (this.accept(TPAREN, '(')) {
this.parseExpression(instr);
this.expect(TPAREN, ')');
Expand Down
13 changes: 9 additions & 4 deletions src/parsing/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export class Parser {
public binaryOps: Record<string, OperatorFunction>;
public ternaryOps: Record<string, OperatorFunction>;
public functions: Record<string, OperatorFunction>;
public consts: Record<string, Value>;
public numericConstants: Record<string, Value>;
public buildInLiterals: Record<string, Value>;
public resolve: VariableResolver;

/**
Expand Down Expand Up @@ -220,11 +221,15 @@ export class Parser {
padRight: padRight
};

this.consts = {
this.numericConstants = {
E: Math.E,
PI: Math.PI,
'true': true,
'false': false
};

this.buildInLiterals = {
true: true,
false: false,
null: null,
};

// A callback that evaluate will call if it doesn't recognize a variable. The default
Expand Down
Loading