From 2ffb39b3766c4f5880a4f25b69d5ded4a170ace0 Mon Sep 17 00:00:00 2001 From: Ramin Barati Date: Sun, 24 May 2026 22:53:18 +0200 Subject: [PATCH] enhance constraints to parse references --- package.json | 2 +- src/ast.ts | 19 +++++- src/index.ts | 4 ++ src/visitor.ts | 29 +++++++- tests/visitor.test.ts | 154 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 194 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 9245955..61d43e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "spex-parser", "repository": "github:Freelansys/spex-parser", - "version": "0.1.1", + "version": "0.1.2", "description": "A parser for Spex, a language designed for programming with LLMs", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/ast.ts b/src/ast.ts index e709a3d..ae21c8d 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -55,10 +55,27 @@ export type ExponentialObject = { exponent: ObjectExpression } +export type ConstraintReference = { + kind: 'ConstraintReference' + name: string +} + +export type ConstraintText = { + kind: 'ConstraintText' + text: string +} + +export type ConstraintPart = ConstraintReference | ConstraintText + +export type Constraint = { + raw: string + parts: ConstraintPart[] +} + export type SubObject = { kind: 'SubObject' base: ObjectExpression - constraint: string + constraint: Constraint } export type ArrayObject = { diff --git a/src/index.ts b/src/index.ts index d7ab708..e577343 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,4 +10,8 @@ export type { ProductObject, ExponentialObject, SubObject, + Constraint, + ConstraintPart, + ConstraintReference, + ConstraintText, } from './ast.js' diff --git a/src/visitor.ts b/src/visitor.ts index 2b12168..f7f416e 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -11,6 +11,9 @@ import type { ProductObject, SubObject, ArrayObject, + Constraint, + ConstraintPart, + ConstraintReference, } from './ast.js' import { SpexLexer } from './lexer.js' import { SpexParser } from './parser.js' @@ -18,6 +21,28 @@ import { SpexParser } from './parser.js' const parserInstance = new SpexParser() const BaseSpexVisitor = parserInstance.getBaseCstVisitorConstructor() +export const REFERENCE_PATTERN = /@([a-zA-Z_][\w]*(?:\.[a-zA-Z_][\w]*)*)/g + +export function parseConstraint(raw: string): Constraint { + const parts: ConstraintPart[] = [] + let lastIndex = 0 + let match: RegExpExecArray | null + + while ((match = REFERENCE_PATTERN.exec(raw)) !== null) { + if (match.index > lastIndex) { + parts.push({ kind: 'ConstraintText', text: raw.slice(lastIndex, match.index) }) + } + parts.push({ kind: 'ConstraintReference', name: match[1]! }) + lastIndex = match.index + match[0].length + } + + if (lastIndex < raw.length) { + parts.push({ kind: 'ConstraintText', text: raw.slice(lastIndex) }) + } + + return { raw, parts } +} + export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor { constructor() { super() @@ -112,11 +137,11 @@ export class SpexParserVisitor extends BaseSpexVisitor implements ICstVisitor { @@ -92,7 +93,10 @@ describe('SpexParserVisitor', () => { p: { kind: 'SubObject', base: { kind: 'NamedObject', name: 'Number' }, - constraint: 'value is positive', + constraint: { + raw: 'value is positive', + parts: [{ kind: 'ConstraintText', text: 'value is positive' }], + }, }, n: { kind: 'NamedObject', name: 'Number' }, }, @@ -178,12 +182,18 @@ describe('SpexParserVisitor', () => { exponent: { kind: 'SubObject', base: { kind: 'NamedObject', name: 'Number' }, - constraint: 'value is positive', + constraint: { + raw: 'value is positive', + parts: [{ kind: 'ConstraintText', text: 'value is positive' }], + }, }, base: { kind: 'SubObject', base: { kind: 'NamedObject', name: 'Number' }, - constraint: 'value is positive', + constraint: { + raw: 'value is positive', + parts: [{ kind: 'ConstraintText', text: 'value is positive' }], + }, }, }, }) @@ -199,7 +209,10 @@ describe('SpexParserVisitor', () => { object: { kind: 'SubObject', base: { kind: 'NamedObject', name: 'Number' }, - constraint: 'isPositive', + constraint: { + raw: 'isPositive', + parts: [{ kind: 'ConstraintText', text: 'isPositive' }], + }, }, }) }) @@ -214,7 +227,10 @@ describe('SpexParserVisitor', () => { object: { kind: 'SubObject', base: { kind: 'NamedObject', name: 'Number' }, - constraint: 'the number is positive', + constraint: { + raw: 'the number is positive', + parts: [{ kind: 'ConstraintText', text: 'the number is positive' }], + }, }, }) }) @@ -236,7 +252,13 @@ describe('SpexParserVisitor', () => { s: { kind: 'NamedObject', name: 'String' }, }, }, - constraint: '@n is positive', + constraint: { + raw: '@n is positive', + parts: [ + { kind: 'ConstraintReference', name: 'n' }, + { kind: 'ConstraintText', text: ' is positive' }, + ], + }, }, }) }) @@ -262,7 +284,10 @@ describe('SpexParserVisitor', () => { }, base: { kind: 'NamedObject', name: 'Bool' }, }, - constraint: 'logs the given input', + constraint: { + raw: 'logs the given input', + parts: [{ kind: 'ConstraintText', text: 'logs the given input' }], + }, }, }) }) @@ -280,9 +305,15 @@ describe('SpexParserVisitor', () => { base: { kind: 'SubObject', base: { kind: 'NamedObject', name: 'Number' }, - constraint: 'value is positive', + constraint: { + raw: 'value is positive', + parts: [{ kind: 'ConstraintText', text: 'value is positive' }], + }, + }, + constraint: { + raw: 'value is odd', + parts: [{ kind: 'ConstraintText', text: 'value is odd' }], }, - constraint: 'value is odd', }, }) }) @@ -460,4 +491,107 @@ describe('SpexParserVisitor', () => { }) }) }) + + describe('parseConstraint', () => { + it('should parse plain text constraint with no references', () => { + const result = parseConstraint('value is positive') + expect(result).toEqual({ + raw: 'value is positive', + parts: [{ kind: 'ConstraintText', text: 'value is positive' }], + }) + }) + + it('should parse constraint with single reference', () => { + const result = parseConstraint('@n is positive') + expect(result).toEqual({ + raw: '@n is positive', + parts: [ + { kind: 'ConstraintReference', name: 'n' }, + { kind: 'ConstraintText', text: ' is positive' }, + ], + }) + }) + + it('should parse constraint with reference in the middle', () => { + const result = parseConstraint('use @path for storage') + expect(result).toEqual({ + raw: 'use @path for storage', + parts: [ + { kind: 'ConstraintText', text: 'use ' }, + { kind: 'ConstraintReference', name: 'path' }, + { kind: 'ConstraintText', text: ' for storage' }, + ], + }) + }) + + it('should parse constraint with dotted references', () => { + const result = parseConstraint('return @z.real^2 + @z.imag^2') + expect(result).toEqual({ + raw: 'return @z.real^2 + @z.imag^2', + parts: [ + { kind: 'ConstraintText', text: 'return ' }, + { kind: 'ConstraintReference', name: 'z.real' }, + { kind: 'ConstraintText', text: '^2 + ' }, + { kind: 'ConstraintReference', name: 'z.imag' }, + { kind: 'ConstraintText', text: '^2' }, + ], + }) + }) + + it('should parse constraint with multiple references', () => { + const result = parseConstraint('call @LoadTodos using @path') + expect(result).toEqual({ + raw: 'call @LoadTodos using @path', + parts: [ + { kind: 'ConstraintText', text: 'call ' }, + { kind: 'ConstraintReference', name: 'LoadTodos' }, + { kind: 'ConstraintText', text: ' using ' }, + { kind: 'ConstraintReference', name: 'path' }, + ], + }) + }) + + it('should parse constraint with reference at start', () => { + const result = parseConstraint('@validate the input') + expect(result).toEqual({ + raw: '@validate the input', + parts: [ + { kind: 'ConstraintReference', name: 'validate' }, + { kind: 'ConstraintText', text: ' the input' }, + ], + }) + }) + + it('should parse constraint with no @ symbols', () => { + const result = parseConstraint('the user is authenticated') + expect(result).toEqual({ + raw: 'the user is authenticated', + parts: [{ kind: 'ConstraintText', text: 'the user is authenticated' }], + }) + }) + + it('should parse constraint with underscore in reference', () => { + const result = parseConstraint('@todo_item is valid') + expect(result).toEqual({ + raw: '@todo_item is valid', + parts: [ + { kind: 'ConstraintReference', name: 'todo_item' }, + { kind: 'ConstraintText', text: ' is valid' }, + ], + }) + }) + + it('should parse empty constraint', () => { + const result = parseConstraint('') + expect(result).toEqual({ + raw: '', + parts: [], + }) + }) + + it('should preserve raw text exactly', () => { + const result = parseConstraint(' @foo ') + expect(result.raw).toBe(' @foo ') + }) + }) })