From 56293e6c4a37f0fc86d1a7d657bbf207edb2fb57 Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Mon, 12 Jan 2026 10:12:19 +0100 Subject: [PATCH 1/3] Document inline functions --- docs/syntax.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/syntax.md b/docs/syntax.md index 0c8f3d2..f3c113b 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -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: From b8a524512180161610150125a7e23ddb320c1fda Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Mon, 12 Jan 2026 13:36:57 +0100 Subject: [PATCH 2/3] Support null values and rename INUMBER to ISCALAR --- src/core/evaluate.ts | 6 +-- src/core/expression-to-string.ts | 6 +-- src/core/simplify.ts | 16 ++++---- .../language-service.documentation.ts | 3 +- src/language-service/language-service.ts | 36 ++++++++++-------- .../language-service.types.ts | 2 +- src/parsing/instruction.ts | 24 +++++------- src/parsing/parser-state.ts | 17 +++++---- src/parsing/parser.ts | 13 +++++-- src/parsing/token-stream.ts | 36 +++++++++++++++--- src/parsing/token.ts | 5 ++- test/operators/operators-comparison.ts | 8 +++- test/parsing/instruction.ts | 38 +++++++++---------- 13 files changed, 124 insertions(+), 86 deletions(-) diff --git a/src/core/evaluate.ts b/src/core/evaluate.ts index 5e9f364..d75786d 100644 --- a/src/core/evaluate.ts +++ b/src/core/evaluate.ts @@ -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 @@ -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(); diff --git a/src/core/expression-to-string.ts b/src/core/expression-to-string.ts index 94f3178..c694815 100644 --- a/src/core/expression-to-string.ts +++ b/src/core/expression-to-string.ts @@ -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 { @@ -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)) { diff --git a/src/core/simplify.ts b/src/core/simplify.ts index 0b5f2f9..7483c21 100644 --- a/src/core/simplify.ts +++ b/src/core/simplify.ts @@ -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( @@ -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, @@ -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()!; @@ -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) { @@ -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()!); diff --git a/src/language-service/language-service.documentation.ts b/src/language-service/language-service.documentation.ts index 8cabe4b..42a4509 100644 --- a/src/language-service/language-service.documentation.ts +++ b/src/language-service/language-service.documentation.ts @@ -224,5 +224,6 @@ export const DEFAULT_CONSTANT_DOCS: Record = { E: 'Math.E', PI: 'Math.PI', true: 'Logical true', - false: 'Logical false' + false: 'Logical false', + null: 'Null value' }; diff --git a/src/language-service/language-service.ts b/src/language-service/language-service.ts index 050852a..353d6df 100644 --- a/src/language-service/language-service.ts +++ b/src/language-service/language-service.ts @@ -2,17 +2,17 @@ // Provides: completions, hover, and syntax highlighting using the existing tokenizer import { - TOP, - TNUMBER, - TSTRING, - TPAREN, - TBRACKET, - TCOMMA, - TNAME, - TSEMICOLON, - TKEYWORD, - TBRACE, - Token + TOP, + TNUMBER, + TSTRING, + TPAREN, + TBRACKET, + TCOMMA, + TNAME, + TSEMICOLON, + TKEYWORD, + TBRACE, + Token, TCONST } from '../parsing'; import { Parser } from '../parsing/parser'; import type { @@ -97,7 +97,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; } @@ -107,6 +109,8 @@ export function createLanguageService(options: LanguageServiceOptions | undefine return 'number'; case TSTRING: return 'string'; + case TCONST: + return 'constant'; case TKEYWORD: return 'keyword'; case TOP: @@ -144,7 +148,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 } })); @@ -237,12 +241,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, @@ -270,7 +274,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 }; } diff --git a/src/language-service/language-service.types.ts b/src/language-service/language-service.types.ts index af29ef1..c26208a 100644 --- a/src/language-service/language-service.types.ts +++ b/src/language-service/language-service.types.ts @@ -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; diff --git a/src/parsing/instruction.ts b/src/parsing/instruction.ts index 7b009a4..aca49ee 100644 --- a/src/parsing/instruction.ts +++ b/src/parsing/instruction.ts @@ -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 @@ -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., +, -, *, /) */ @@ -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 @@ -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; } @@ -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; } /** @@ -262,7 +256,7 @@ export class Instruction { toString(): string { switch (this.type) { - case INUMBER: + case ISCALAR: case IOP1: case IOP2: case IOP3: @@ -314,7 +308,7 @@ export function ternaryInstruction(value: string): Instruction { } export function numberInstruction(value: number): Instruction { - return new Instruction(INUMBER, value); + return new Instruction(ISCALAR, value); } export function variableInstruction(value: string): Instruction { diff --git a/src/parsing/parser-state.ts b/src/parsing/parser-state.ts index ad9f258..331ebbf 100644 --- a/src/parsing/parser-state.ts +++ b/src/parsing/parser-state.ts @@ -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'; @@ -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, ')'); diff --git a/src/parsing/parser.ts b/src/parsing/parser.ts index 476d5cb..289a94f 100644 --- a/src/parsing/parser.ts +++ b/src/parsing/parser.ts @@ -83,7 +83,8 @@ export class Parser { public binaryOps: Record; public ternaryOps: Record; public functions: Record; - public consts: Record; + public numericConstants: Record; + public buildInLiterals: Record; public resolve: VariableResolver; /** @@ -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 diff --git a/src/parsing/token-stream.ts b/src/parsing/token-stream.ts index 0cf450f..5dc914d 100644 --- a/src/parsing/token-stream.ts +++ b/src/parsing/token-stream.ts @@ -1,6 +1,22 @@ // cSpell:words TEOF TNUMBER TSTRING TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE -import { Token, TEOF, TOP, TNUMBER, TSTRING, TPAREN, TBRACKET, TCOMMA, TNAME, TSEMICOLON, TKEYWORD, TBRACE, TokenType, TokenValue } from './token.js'; +import { + Token, + TEOF, + TOP, + TNUMBER, + TSTRING, + TPAREN, + TBRACKET, + TCOMMA, + TNAME, + TSEMICOLON, + TKEYWORD, + TBRACE, + TokenType, + TokenValue, + TCONST +} from './token.js'; import { ParseError } from '../types/errors.js'; import type { OperatorFunction } from '../types/parser.js'; @@ -13,7 +29,8 @@ interface ParserLike { unaryOps: Record; binaryOps: Record; ternaryOps: Record; - consts: Record; + numericConstants: Record; + buildInLiterals: Record; options: { allowMemberAccess?: boolean; operators?: Record; @@ -56,7 +73,8 @@ export class TokenStream { public unaryOps: Record; public binaryOps: Record; public ternaryOps: Record; - public consts: Record; + public numericConstants: Record; + public buildInLiterals: Record; public expression: string; public savedPosition: number = 0; public savedCurrent: Token | null = null; @@ -68,7 +86,8 @@ export class TokenStream { this.unaryOps = parser.unaryOps; this.binaryOps = parser.binaryOps; this.ternaryOps = parser.ternaryOps; - this.consts = parser.consts; + this.numericConstants = parser.numericConstants; + this.buildInLiterals = parser.buildInLiterals; this.expression = expression; this.options = parser.options; this.parser = parser; @@ -197,8 +216,13 @@ export class TokenStream { } if (i > startPos) { const str = this.expression.substring(startPos, i); - if (str in this.consts) { - this.current = this.newToken(TNUMBER, this.consts[str]); + if (str in this.numericConstants) { + this.current = this.newToken(TNUMBER, this.numericConstants[str]); + this.pos += str.length; + return true; + } + if (str in this.buildInLiterals) { + this.current = this.newToken(TCONST, this.buildInLiterals[str]); this.pos += str.length; return true; } diff --git a/src/parsing/token.ts b/src/parsing/token.ts index 6bca522..b3c17d4 100644 --- a/src/parsing/token.ts +++ b/src/parsing/token.ts @@ -1,4 +1,4 @@ -// cSpell:words TEOF TNUMBER TSTRING TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE +// cSpell:words TEOF TNUMBER TSTRING TCONST TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE /** * Token types for the expression lexer/tokenizer. @@ -22,6 +22,8 @@ export const TOP = 'TOP' as const; export const TNUMBER = 'TNUMBER' as const; /** String literal token */ export const TSTRING = 'TSTRING' as const; +/** Constant literal token */ +export const TCONST = 'TCONST' as const; /** Parenthesis token ( or ) */ export const TPAREN = 'TPAREN' as const; /** Bracket token [ or ] */ @@ -45,6 +47,7 @@ export type TokenType = | typeof TOP | typeof TNUMBER | typeof TSTRING + | typeof TCONST | typeof TPAREN | typeof TBRACKET | typeof TCOMMA diff --git a/test/operators/operators-comparison.ts b/test/operators/operators-comparison.ts index 83d9356..e136420 100644 --- a/test/operators/operators-comparison.ts +++ b/test/operators/operators-comparison.ts @@ -34,9 +34,15 @@ describe('Comparison Operators TypeScript Test', () => { expect(Parser.evaluate('\'3\' == \'3\'')).toBe(true); }); - it('null == null', () => { + it('null == null (variables)', () => { expect(Parser.evaluate('null == alsoNull', { null: null, alsoNull: null })).toBe(true); }); + it('null == null (single variable)', () => { + expect(Parser.evaluate('null == alsoNull', { alsoNull: null })).toBe(true); + }); + it('null == null (no variables)', () => { + expect(Parser.evaluate('null == null')).toBe(true); + }); }); describe('!= operator', () => { diff --git a/test/parsing/instruction.ts b/test/parsing/instruction.ts index b532fb4..153fde3 100644 --- a/test/parsing/instruction.ts +++ b/test/parsing/instruction.ts @@ -9,7 +9,7 @@ import { functionCallInstruction, arrayInstruction, memberInstruction, - INUMBER, + ISCALAR, IOP1, IOP2, IOP3, @@ -33,14 +33,14 @@ import { describe('Instruction', () => { describe('constructor and basic properties', () => { it('should create instruction with type and value', () => { - const instruction = new Instruction(INUMBER, 42); - expect(instruction.type).toBe(INUMBER); + const instruction = new Instruction(ISCALAR, 42); + expect(instruction.type).toBe(ISCALAR); expect(instruction.value).toBe(42); }); it('should handle null value correctly', () => { - const instruction = new Instruction(INUMBER, null); - expect(instruction.value).toBe(0); + const instruction = new Instruction(ISCALAR, null); + expect(instruction.value).toBe(null); }); it('should preserve undefined value', () => { @@ -49,25 +49,25 @@ describe('Instruction', () => { }); it('should preserve zero value', () => { - const instruction = new Instruction(INUMBER, 0); + const instruction = new Instruction(ISCALAR, 0); expect(instruction.value).toBe(0); }); }); describe('is() type guard method', () => { it('should correctly identify matching instruction type', () => { - const instruction = new Instruction(INUMBER, 42); - expect(instruction.is(INUMBER)).toBe(true); + const instruction = new Instruction(ISCALAR, 42); + expect(instruction.is(ISCALAR)).toBe(true); }); it('should correctly reject non-matching instruction type', () => { - const instruction = new Instruction(INUMBER, 42); + const instruction = new Instruction(ISCALAR, 42); expect(instruction.is(IVAR)).toBe(false); }); it('should work with all instruction types', () => { const types = [ - INUMBER, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, + ISCALAR, IOP1, IOP2, IOP3, IVAR, IVARNAME, IFUNCALL, IFUNDEF, IARRAY, IMEMBER, IUNDEFINED, IENDSTATEMENT, ICASECOND, ICASEMATCH, IWHENCOND, IWHENMATCH, ICASEELSE, IPROPERTY, IOBJECT ]; @@ -77,7 +77,7 @@ describe('Instruction', () => { expect(instruction.is(type)).toBe(true); // Test that it returns false for a different type - const otherType = types.find(t => t !== type) || INUMBER; + const otherType = types.find(t => t !== type) || ISCALAR; expect(instruction.is(otherType)).toBe(false); }); }); @@ -85,13 +85,13 @@ describe('Instruction', () => { describe('getValue() method', () => { it('should return value when type matches', () => { - const instruction = new Instruction(INUMBER, 42); - expect(instruction.getValue(INUMBER)).toBe(42); + const instruction = new Instruction(ISCALAR, 42); + expect(instruction.getValue(ISCALAR)).toBe(42); }); it('should throw error when type does not match', () => { - const instruction = new Instruction(INUMBER, 42); - expect(() => instruction.getValue(IVAR)).toThrow('Expected instruction type IVAR, got INUMBER'); + const instruction = new Instruction(ISCALAR, 42); + expect(() => instruction.getValue(IVAR)).toThrow('Expected instruction type IVAR, got ISCALAR'); }); it('should work with string values', () => { @@ -106,8 +106,8 @@ describe('Instruction', () => { }); describe('toString() method', () => { - it('should return value string for INUMBER', () => { - const instruction = new Instruction(INUMBER, 42); + it('should return value string for ISCALAR', () => { + const instruction = new Instruction(ISCALAR, 42); expect(instruction.toString()).toBe(42); }); @@ -228,7 +228,7 @@ describe('Instruction', () => { it('should create number instruction', () => { const instruction = numberInstruction(42); - expect(instruction.type).toBe(INUMBER); + expect(instruction.type).toBe(ISCALAR); expect(instruction.value).toBe(42); }); @@ -288,7 +288,7 @@ describe('Instruction', () => { it('should handle all instruction types with type guards', () => { const testCases = [ - { type: INUMBER, value: 42 }, + { type: ISCALAR, value: 42 }, { type: IOP1, value: 'abs' }, { type: IOP2, value: '*' }, { type: IOP3, value: '?' }, From a030b037cd2dcdeb20cf33c1115ca199005439a6 Mon Sep 17 00:00:00 2001 From: Sander Toonen Date: Mon, 12 Jan 2026 16:12:06 +0100 Subject: [PATCH 3/3] Small beauty fixes --- src/language-service/language-service.ts | 23 +++++---- src/parsing/instruction.ts | 4 ++ src/parsing/token-stream.ts | 30 +++++------ test/operators/operators-comparison.ts | 6 +++ test/parsing/instruction.ts | 66 +++++++++++++----------- 5 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/language-service/language-service.ts b/src/language-service/language-service.ts index 353d6df..9ff617c 100644 --- a/src/language-service/language-service.ts +++ b/src/language-service/language-service.ts @@ -2,17 +2,18 @@ // Provides: completions, hover, and syntax highlighting using the existing tokenizer import { - TOP, - TNUMBER, - TSTRING, - TPAREN, - TBRACKET, - TCOMMA, - TNAME, - TSEMICOLON, - TKEYWORD, - TBRACE, - Token, TCONST + TOP, + TNUMBER, + TCONST, + TSTRING, + TPAREN, + TBRACKET, + TCOMMA, + TNAME, + TSEMICOLON, + TKEYWORD, + TBRACE, + Token } from '../parsing'; import { Parser } from '../parsing/parser'; import type { diff --git a/src/parsing/instruction.ts b/src/parsing/instruction.ts index aca49ee..764ca49 100644 --- a/src/parsing/instruction.ts +++ b/src/parsing/instruction.ts @@ -311,6 +311,10 @@ export function numberInstruction(value: number): Instruction { return new Instruction(ISCALAR, value); } +export function scalarInstruction(value: boolean | null): Instruction { + return new Instruction(ISCALAR, value); +} + export function variableInstruction(value: string): Instruction { return new Instruction(IVAR, value); } diff --git a/src/parsing/token-stream.ts b/src/parsing/token-stream.ts index 5dc914d..727c0e6 100644 --- a/src/parsing/token-stream.ts +++ b/src/parsing/token-stream.ts @@ -1,21 +1,21 @@ // cSpell:words TEOF TNUMBER TSTRING TPAREN TBRACKET TCOMMA TNAME TSEMICOLON TUNDEFINED TKEYWORD TBRACE import { - Token, - TEOF, - TOP, - TNUMBER, - TSTRING, - TPAREN, - TBRACKET, - TCOMMA, - TNAME, - TSEMICOLON, - TKEYWORD, - TBRACE, - TokenType, - TokenValue, - TCONST + Token, + TEOF, + TOP, + TCONST, + TNUMBER, + TSTRING, + TPAREN, + TBRACKET, + TCOMMA, + TNAME, + TSEMICOLON, + TKEYWORD, + TBRACE, + TokenType, + TokenValue } from './token.js'; import { ParseError } from '../types/errors.js'; import type { OperatorFunction } from '../types/parser.js'; diff --git a/test/operators/operators-comparison.ts b/test/operators/operators-comparison.ts index e136420..d913391 100644 --- a/test/operators/operators-comparison.ts +++ b/test/operators/operators-comparison.ts @@ -43,6 +43,12 @@ describe('Comparison Operators TypeScript Test', () => { it('null == null (no variables)', () => { expect(Parser.evaluate('null == null')).toBe(true); }); + it('null cannot be overridden', () => { + expect(Parser.evaluate('null == alsoNull', { null: 100, alsoNull: null })).toBe(true); + }); + it('null differs from 0', () => { + expect(Parser.evaluate('null == zero', { zero: 0 })).toBe(false); + }); }); describe('!= operator', () => { diff --git a/test/parsing/instruction.ts b/test/parsing/instruction.ts index 153fde3..ea7f9db 100644 --- a/test/parsing/instruction.ts +++ b/test/parsing/instruction.ts @@ -1,33 +1,33 @@ import { describe, it, expect } from 'vitest'; import { - Instruction, - unaryInstruction, - binaryInstruction, - ternaryInstruction, - numberInstruction, - variableInstruction, - functionCallInstruction, - arrayInstruction, - memberInstruction, - ISCALAR, - IOP1, - IOP2, - IOP3, - IVAR, - IVARNAME, - IFUNCALL, - IFUNDEF, - IARRAY, - IMEMBER, - IUNDEFINED, - IENDSTATEMENT, - ICASECOND, - ICASEMATCH, - IWHENCOND, - IWHENMATCH, - ICASEELSE, - IPROPERTY, - IOBJECT + Instruction, + unaryInstruction, + binaryInstruction, + ternaryInstruction, + numberInstruction, + variableInstruction, + functionCallInstruction, + arrayInstruction, + memberInstruction, + ISCALAR, + IOP1, + IOP2, + IOP3, + IVAR, + IVARNAME, + IFUNCALL, + IFUNDEF, + IARRAY, + IMEMBER, + IUNDEFINED, + IENDSTATEMENT, + ICASECOND, + ICASEMATCH, + IWHENCOND, + IWHENMATCH, + ICASEELSE, + IPROPERTY, + IOBJECT, scalarInstruction } from '../../src/parsing/instruction.js'; describe('Instruction', () => { @@ -38,7 +38,7 @@ describe('Instruction', () => { expect(instruction.value).toBe(42); }); - it('should handle null value correctly', () => { + it('should preserve null value', () => { const instruction = new Instruction(ISCALAR, null); expect(instruction.value).toBe(null); }); @@ -232,6 +232,12 @@ describe('Instruction', () => { expect(instruction.value).toBe(42); }); + it('should create scalar instruction', () => { + const instruction = scalarInstruction(true); + expect(instruction.type).toBe(ISCALAR); + expect(instruction.value).toBe(true); + }); + it('should create variable instruction', () => { const instruction = variableInstruction('myVar'); expect(instruction.type).toBe(IVAR); @@ -289,6 +295,8 @@ describe('Instruction', () => { it('should handle all instruction types with type guards', () => { const testCases = [ { type: ISCALAR, value: 42 }, + { type: ISCALAR, value: false }, + { type: ISCALAR, value: null }, { type: IOP1, value: 'abs' }, { type: IOP2, value: '*' }, { type: IOP3, value: '?' },