/** * S-FEEL 子集的语法分析 —— 递归下降,纯函数(编码规范 §8.3)。 * * 优先级(低 → 高):or < and < 比较/in < 加减 < 乘除 < 一元 < 基元 */ import type { Node, UnaryTest } from './ast.ts'; import { SfeelSyntaxError, tokenize } from './lexer.ts'; import type { Token } from './lexer.ts'; /** 多词函数名 —— 子集封顶,只认这些(OS 03:无外部调用) */ const MULTIWORD = [ ['starts', 'with'], ['ends', 'with'], ['string', 'length'], ['date', 'and', 'time'], ['days', 'and', 'time', 'duration'], ['upper', 'case'], ['lower', 'case'], ] as const; const KEYWORDS = new Set(['and', 'or', 'not', 'true', 'false', 'null', 'in']); class Parser { private index = 0; constructor(private readonly tokens: readonly Token[]) {} private peek(offset = 0): Token { return this.tokens[Math.min(this.index + offset, this.tokens.length - 1)]!; } private next(): Token { const token = this.peek(); this.index += 1; return token; } private eat(text: string): boolean { if (this.peek().text === text && this.peek().kind !== 'string') { this.index += 1; return true; } return false; } private expect(text: string): Token { if (!this.eat(text)) throw new SfeelSyntaxError(`期望 ${text},实际 ${this.peek().text || 'EOF'}`, this.peek().pos); return this.tokens[this.index - 1]!; } atEnd(): boolean { return this.peek().kind === 'eof'; } parseExpression(): Node { return this.parseOr(); } private parseOr(): Node { let left = this.parseAnd(); while (this.peek().kind === 'name' && this.peek().text === 'or') { this.next(); left = { kind: 'binary', op: 'or', left, right: this.parseAnd() }; } return left; } private parseAnd(): Node { let left = this.parseComparison(); while (this.peek().kind === 'name' && this.peek().text === 'and') { this.next(); left = { kind: 'binary', op: 'and', left, right: this.parseComparison() }; } return left; } private parseComparison(): Node { const left = this.parseAdditive(); const token = this.peek(); if (token.kind === 'name' && token.text === 'in') { this.next(); return { kind: 'in', value: left, candidates: this.parseAdditive() }; } const ops = ['<=', '>=', '!=', '==', '<', '>', '='] as const; for (const op of ops) { if (token.kind === 'punct' && token.text === op) { this.next(); const normalized = op === '==' ? '=' : op; return { kind: 'binary', op: normalized, left, right: this.parseAdditive() }; } } return left; } private parseAdditive(): Node { let left = this.parseMultiplicative(); for (;;) { const token = this.peek(); if (token.kind === 'punct' && (token.text === '+' || token.text === '-')) { this.next(); left = { kind: 'binary', op: token.text, left, right: this.parseMultiplicative() }; continue; } return left; } } private parseMultiplicative(): Node { let left = this.parseUnary(); for (;;) { const token = this.peek(); if (token.kind === 'punct' && (token.text === '*' || token.text === '/')) { this.next(); left = { kind: 'binary', op: token.text, left, right: this.parseUnary() }; continue; } return left; } } private parseUnary(): Node { const token = this.peek(); if (token.kind === 'punct' && token.text === '-') { this.next(); return { kind: 'unary', op: '-', operand: this.parseUnary() }; } if (token.kind === 'name' && token.text === 'not') { this.next(); this.expect('('); const operand = this.parseExpression(); this.expect(')'); return { kind: 'unary', op: 'not', operand }; } return this.parsePrimary(); } /** 尝试合并多词函数名;不匹配则返回单词 */ private readName(first: Token): string { for (const words of MULTIWORD) { if (words[0] !== first.text) continue; const matches = words.every((word, offset) => (offset === 0 ? true : this.peek(offset - 1).text === word)); if (matches) { for (let k = 1; k < words.length; k += 1) this.next(); return words.join(' '); } } return first.text; } private parsePrimary(): Node { const token = this.next(); if (token.kind === 'number') return { kind: 'literal', value: token.value as number }; if (token.kind === 'string') return { kind: 'literal', value: token.value as string }; // `(` 兼两职:分组与开区间起点 —— 先解析内部,再看是不是 `..` if (token.kind === 'punct' && token.text === '(') { const inner = this.parseExpression(); if (this.peek().kind === 'punct' && this.peek().text === '..') { this.next(); return this.finishRange(true, inner); } this.expect(')'); return inner; } // 列表或区间:[a, b] / [1..10] if (token.kind === 'punct' && token.text === '[') { return this.parseListOrRange(false); } if (token.kind === 'name') { if (token.text === 'true') return { kind: 'literal', value: true }; if (token.text === 'false') return { kind: 'literal', value: false }; if (token.text === 'null') return { kind: 'literal', value: null }; const name = this.readName(token); if (this.peek().kind === 'punct' && this.peek().text === '(') { this.next(); const args: Node[] = []; if (!(this.peek().kind === 'punct' && this.peek().text === ')')) { args.push(this.parseExpression()); while (this.eat(',')) args.push(this.parseExpression()); } this.expect(')'); return { kind: 'call', name, args }; } if (KEYWORDS.has(name)) throw new SfeelSyntaxError(`关键字 ${name} 不能作为路径`, token.pos); const segments = [name]; while (this.peek().kind === 'punct' && this.peek().text === '.') { this.next(); const part = this.next(); if (part.kind !== 'name') throw new SfeelSyntaxError('路径分段必须是名字', part.pos); segments.push(part.text); } return { kind: 'path', segments }; } throw new SfeelSyntaxError(`意外的记号 ${token.text || 'EOF'}`, token.pos); } private parseListOrRange(startOpen: boolean): Node { // 空列表 if (this.peek().kind === 'punct' && this.peek().text === ']') { this.next(); return { kind: 'list', items: [] }; } const first = this.parseExpression(); if (this.peek().kind === 'punct' && this.peek().text === '..') { this.next(); return this.finishRange(startOpen, first); } const items = [first]; while (this.eat(',')) items.push(this.parseExpression()); this.expect(']'); return { kind: 'list', items }; } /** 区间收尾:`..` 已吃掉,读上界与闭合符 */ private finishRange(startOpen: boolean, start: Node): Node { const end = this.parseExpression(); const closing = this.next(); if (closing.kind !== 'punct' || (closing.text !== ']' && closing.text !== ')')) { throw new SfeelSyntaxError('区间必须以 ] 或 ) 收尾', closing.pos); } return { kind: 'range', startOpen, endOpen: closing.text === ')', start, end }; } /** 单元格谓词:`-` / `<10` / `[1..5]` / `"A","B"` */ parseUnaryTest(): UnaryTest { if (this.peek().kind === 'punct' && this.peek().text === '-' && this.peek(1).kind === 'eof') { this.next(); return { kind: 'any' }; } const tests: UnaryTest[] = [this.parseSingleUnaryTest()]; while (this.eat(',')) tests.push(this.parseSingleUnaryTest()); return tests.length === 1 ? tests[0]! : { kind: 'disjunction', tests }; } private parseSingleUnaryTest(): UnaryTest { const token = this.peek(); const ops = ['<=', '>=', '!=', '<', '>', '='] as const; for (const op of ops) { if (token.kind === 'punct' && token.text === op) { this.next(); return { kind: 'compare', op, operand: this.parseAdditive() }; } } const node = this.parseExpression(); if (node.kind === 'range') return { kind: 'range', node }; return { kind: 'compare', op: '=', operand: node }; } } export function parse(source: string): Node { const parser = new Parser(tokenize(source)); const node = parser.parseExpression(); if (!parser.atEnd()) throw new SfeelSyntaxError('表达式尾部有多余记号', 0); return node; } export function parseUnaryTest(source: string): UnaryTest { const trimmed = source.trim(); if (trimmed === '' || trimmed === '-') return { kind: 'any' }; const parser = new Parser(tokenize(trimmed)); const test = parser.parseUnaryTest(); if (!parser.atEnd()) throw new SfeelSyntaxError('单元格谓词尾部有多余记号', 0); return test; }