From df7c64207783f260b733923af3458149ac5b42b2 Mon Sep 17 00:00:00 2001 From: Asaki Yuki Date: Sat, 17 Jan 2026 02:03:47 +0700 Subject: [PATCH] uh --- package.json | 2 +- src/compilers/bindings/Bindings.ts | 149 ------------------ src/compilers/bindings/Checker.ts | 11 ++ src/compilers/bindings/Funtion.ts | 33 ++++ src/compilers/bindings/Lexer.ts | 126 ++++++++++++++++ src/compilers/bindings/Parser.ts | 235 +++++++++++++++++++++++++++++ src/compilers/bindings/index.ts | 5 +- src/compilers/bindings/types.ts | 22 +++ src/components/Utils.ts | 6 +- test/app.ts | 8 +- 10 files changed, 440 insertions(+), 157 deletions(-) delete mode 100644 src/compilers/bindings/Bindings.ts create mode 100644 src/compilers/bindings/Checker.ts create mode 100644 src/compilers/bindings/Funtion.ts create mode 100644 src/compilers/bindings/Lexer.ts create mode 100644 src/compilers/bindings/Parser.ts diff --git a/package.json b/package.json index 3b8a090..7defb2b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "types": "dist/index.d.ts", "scripts": { "build": "npx tsc", - "watch": "npx tsc --watch", + "build-watch": "npx tsc --watch", "test": "bun test/app.ts", "test-watch": "bun --watch test/app.ts", "prefetch": "bun scripts/prefetch" diff --git a/src/compilers/bindings/Bindings.ts b/src/compilers/bindings/Bindings.ts deleted file mode 100644 index 9aad1fd..0000000 --- a/src/compilers/bindings/Bindings.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Token, TokenKind } from "./types.js" - -export const BindingTranspiler = { - isBlankChar(char: string) { - return /\s/.test(char) - }, - - isWordChar(char: string) { - return char && /\w/.test(char) - }, - - isNumberChar(char: string) { - return /\d/.test(char) - }, - - token(input: string, kind: TokenKind, start: number, length: number = 1): Token { - return { - value: input.slice(start, start + length), - kind, - start, - length, - } - }, - - lexer(input: string) { - const tokens: Token[] = [] - - let index = 0 - do { - const token = input[index] - - if (BindingTranspiler.isBlankChar(token)) continue - - switch (token) { - // Literals - case "#": - case "$": { - const start = index++ - - do { - const token = input[index] - if (BindingTranspiler.isWordChar(token)) continue - else break - } while (++index < input.length) - - tokens.push(BindingTranspiler.token(input, TokenKind.VARIABLE, start, index - start)) - - break - } - - case "'": { - const start = index++ - - do { - const token = input[index] - if (token === "'") break - } while (++index < input.length) - - tokens.push(BindingTranspiler.token(input, TokenKind.STRING, start, index - start + 1)) - - break - } - - case "`": { - const start = index++, - struct: boolean[] = [] - - do { - const token = input[index] - let lastStruct = struct.lastItem() - - if (token === "`") { - if (struct.length) { - if (lastStruct === false) struct.pop() - else struct.push(false) - } else break - } - - if (token === "$") { - if (input[index + 1] === "{" && !lastStruct) { - struct.push(true) - index++ - } - } - - if (token === "}" && lastStruct === true) struct.pop() - } while (++index < input.length) - - tokens.push(BindingTranspiler.token(input, TokenKind.TEMPLATE_STRING, start, index - start + 1)) - - break - } - - case ",": - tokens.push(BindingTranspiler.token(input, TokenKind.COMMA, index)) - break - - // Single operators - case "+": - case "-": - case "*": - case "/": - tokens.push(BindingTranspiler.token(input, TokenKind.OPERATOR, index)) - break - - case "(": - tokens.push(BindingTranspiler.token(input, TokenKind.OPEN_PARENTHESIS, index)) - break - - case ")": - tokens.push(BindingTranspiler.token(input, TokenKind.CLOSE_PARENTHESIS, index)) - break - - // Double operators - case "&": - case "|": - case "=": - if (input[index + 1] === input[index]) - tokens.push(BindingTranspiler.token(input, TokenKind.OPERATOR, ++index, 2)) - else tokens.push(BindingTranspiler.token(input, TokenKind.OPERATOR, index)) - break - - case "!": - case ">": - case "<": - if (input[index + 1] === "=") - tokens.push(BindingTranspiler.token(input, TokenKind.OPERATOR, ++index, 2)) - else tokens.push(BindingTranspiler.token(input, TokenKind.OPERATOR, index)) - break - - default: { - let start = index - - if (BindingTranspiler.isNumberChar(token)) { - while (BindingTranspiler.isNumberChar(input[index + 1])) index++ - tokens.push(BindingTranspiler.token(input, TokenKind.NUMBER, start, index - start + 1)) - } else if (BindingTranspiler.isWordChar(token)) { - while (BindingTranspiler.isWordChar(input[index + 1])) index++ - tokens.push(BindingTranspiler.token(input, TokenKind.WORD, start, index - start + 1)) - } - } - } - } while (++index < input.length) - - return tokens - }, - - parser(input: string, tokens: Token[]) {}, -} diff --git a/src/compilers/bindings/Checker.ts b/src/compilers/bindings/Checker.ts new file mode 100644 index 0000000..542752c --- /dev/null +++ b/src/compilers/bindings/Checker.ts @@ -0,0 +1,11 @@ +export function isBlankChar(char: string) { + return /\s/.test(char) +} + +export function isWordChar(char: string) { + return char && /\w/.test(char) +} + +export function isNumberChar(char: string) { + return /\d/.test(char) +} diff --git a/src/compilers/bindings/Funtion.ts b/src/compilers/bindings/Funtion.ts new file mode 100644 index 0000000..c06db6a --- /dev/null +++ b/src/compilers/bindings/Funtion.ts @@ -0,0 +1,33 @@ +import { RandomBindingString } from "../../components/Utils.js" +import { Expression, GenBinding } from "./types.js" + +export const FuntionMap = new Map< + string, + (...args: Expression[]) => { + genBindings?: GenBinding[] + value: Expression + } +>() + +// Default Functions +FuntionMap.set("abs", number => { + const randomBinding = RandomBindingString(16) + return { + genBindings: [{ source: `((-1 + (${number} > 0) * 2) * ${number})`, target: randomBinding }], + value: randomBinding, + } +}) + +FuntionMap.set("new", expression => { + const randomBinding = RandomBindingString(16) + return { + genBindings: [{ source: expression, target: randomBinding }], + value: randomBinding, + } +}) + +FuntionMap.set("max", (...args) => { + return { + value: "#a", + } +}) diff --git a/src/compilers/bindings/Lexer.ts b/src/compilers/bindings/Lexer.ts new file mode 100644 index 0000000..cd12faa --- /dev/null +++ b/src/compilers/bindings/Lexer.ts @@ -0,0 +1,126 @@ +import { makeToken, TokenKind, Token } from "./types.js" +import * as Checker from "./Checker.js" + +export function Lexer(input: string) { + const tokens: Token[] = [] + + if (input.length === 0) return tokens + + let index = 0 + do { + const token = input[index] + + if (Checker.isBlankChar(token)) continue + + switch (token) { + // Literals + case "#": + case "$": { + const start = index++ + + while (index < input.length) { + const token = input[index] + if (Checker.isWordChar(token)) index++ + else break + } + + tokens.push(makeToken(input, TokenKind.VARIABLE, start, index-- - start)) + + break + } + + case "'": { + const start = index++ + + do { + const token = input[index] + if (token === "'") break + } while (++index < input.length) + + tokens.push(makeToken(input, TokenKind.STRING, start, index - start + 1)) + + break + } + + case "`": { + const start = index++, + struct: boolean[] = [] + + do { + const token = input[index] + let lastStruct = struct.lastItem() + + if (token === "`") { + if (struct.length) { + if (lastStruct === false) struct.pop() + else struct.push(false) + } else break + } + + if (token === "$") { + if (input[index + 1] === "{" && !lastStruct) { + struct.push(true) + index++ + } + } + + if (token === "}" && lastStruct === true) struct.pop() + } while (++index < input.length) + + tokens.push(makeToken(input, TokenKind.TEMPLATE_STRING, start, index - start + 1)) + + break + } + + case ",": + tokens.push(makeToken(input, TokenKind.COMMA, index)) + break + + // Single operators + case "+": + case "-": + case "*": + case "/": + case "%": + tokens.push(makeToken(input, TokenKind.OPERATOR, index)) + break + + case "(": + tokens.push(makeToken(input, TokenKind.OPEN_PARENTHESIS, index)) + break + + case ")": + tokens.push(makeToken(input, TokenKind.CLOSE_PARENTHESIS, index)) + break + + // Double operators + case "&": + case "|": + case "=": + if (input[index + 1] === input[index]) tokens.push(makeToken(input, TokenKind.OPERATOR, index++, 2)) + else tokens.push(makeToken(input, TokenKind.OPERATOR, index)) + break + + case "!": + case ">": + case "<": + if (input[index + 1] === "=") tokens.push(makeToken(input, TokenKind.OPERATOR, index++, 2)) + else tokens.push(makeToken(input, TokenKind.OPERATOR, index)) + break + + default: { + let start = index + + if (Checker.isNumberChar(token)) { + while (Checker.isNumberChar(input[index + 1])) index++ + tokens.push(makeToken(input, TokenKind.NUMBER, start, index - start + 1)) + } else if (Checker.isWordChar(token)) { + while (Checker.isWordChar(input[index + 1])) index++ + tokens.push(makeToken(input, TokenKind.WORD, start, index - start + 1)) + } + } + } + } while (++index < input.length) + + return tokens +} diff --git a/src/compilers/bindings/Parser.ts b/src/compilers/bindings/Parser.ts new file mode 100644 index 0000000..7c62930 --- /dev/null +++ b/src/compilers/bindings/Parser.ts @@ -0,0 +1,235 @@ +import { BindingType } from "../../types/enums/BindingType.js" +import { BindingItem } from "../../types/properties/value.js" +import { FuntionMap } from "./Funtion.js" +import { Lexer } from "./Lexer.js" +import { Expression, GenBinding, Token, TokenKind } from "./types.js" + +export class Parser { + position: number = 0 + tokens: Token[] + + genBindings: GenBinding[] = [] + output: Expression + + constructor(private input: string) { + this.tokens = Lexer(this.input) + this.output = this.parseExpression() + } + + at() { + return this.tokens[this.position] + } + + eat() { + return this.tokens[this.position++] + } + + last() { + return this.tokens[this.tokens.length - 1] + } + + private parseExpression(): Expression { + return this.parseAdditiveExpression() + } + + private parseAdditiveExpression(): Expression { + let left = this.parseMultiplicativeExpression(), + current + + while ( + (current = this.at()) && + this.at()?.kind === TokenKind.OPERATOR && + ["+", "-", "==", ">", "<", ">=", "<=", "!=", "&&", "||"].includes(current.value) + ) { + const operator = this.eat() + const right = this.parseMultiplicativeExpression() + + switch (operator.value) { + case "==": + left = `(${left} = ${right})` + break + + case ">=": + case "<=": + left = `((${left} ${operator.value[0]} ${right}) or (${left} = ${right}))` + break + + case "!=": + left = `(not (${left} = ${right}))` + break + + case "&&": + left = `(${left} and ${right})` + break + + case "||": + left = `(${left} or ${right})` + break + + default: + left = `(${left} ${operator.value} ${right})` + break + } + } + + return left + } + + private parseMultiplicativeExpression(): Expression { + let left = this.parsePrimaryExpression(), + current + + while ( + (current = this.at()) && + this.at()?.kind === TokenKind.OPERATOR && + ["*", "/", "%"].includes(current.value) + ) { + const operator = this.eat() + const right = this.parsePrimaryExpression() + + if (current.value === "%") left = `(${left} - ${left} / ${right} * ${right})` + else left = `(${left} ${operator.value} ${right})` + } + + return left + } + + private parsePrimaryExpression(): Expression { + const left = this.at() + + switch (left?.kind) { + case TokenKind.WORD: + return this.parseCallExpression(this.eat()) + + case TokenKind.OPEN_PARENTHESIS: { + this.eat() + const value = this.parseExpression() + this.expect(TokenKind.CLOSE_PARENTHESIS, "Unexpected token!") + this.eat() + return `(${value})` + } + + case TokenKind.OPERATOR: { + if (left.value === "-" || left.value === "+") { + this.eat() + + let negative = left.value === "-", + current + + while ((current = this.at()) && current.kind === TokenKind.OPERATOR) { + if (current.value === "-") { + this.eat() + negative = !negative + } else if (current.value === "+") { + this.eat() + continue + } + } + + const value = this.parsePrimaryExpression() + + return negative ? `(0 - ${value})` : value + } else if (left.value === "!") { + this.eat() + + let not = true, + current + + while ((current = this.at()) && current.kind === TokenKind.OPERATOR) { + if (current.value === "!") { + this.eat() + not = !not + } + } + + const value = this.parsePrimaryExpression() + return not ? `(not ${value})` : value + } else break + } + + case TokenKind.VARIABLE: + case TokenKind.NUMBER: + case TokenKind.STRING: + return this.eat().value + + case undefined: + this.expect(TokenKind.NUMBER, "Unexpected token!") + } + + return left.value + } + + private parseCallExpression(callerToken: Token): Expression { + const left = this.at() + + if (left?.kind === TokenKind.OPEN_PARENTHESIS) { + this.eat() + const args: Expression[] = [] + + if (this.at().kind !== TokenKind.CLOSE_PARENTHESIS) { + args.push(this.parseExpression()) + + while (this.at().kind === TokenKind.COMMA) { + this.eat() + if (this.at().kind === TokenKind.CLOSE_PARENTHESIS) { + this.expect(TokenKind.CLOSE_PARENTHESIS, "Unexpected token!") + } + args.push(this.parseExpression()) + } + } + + this.eat() + + return this.funtionCall(callerToken.value, ...args) + } else { + this.warn("This token should be a string!", callerToken) + return callerToken.value + } + } + + private funtionCall(name: string, ...params: Expression[]): Expression { + const func = FuntionMap.get(name) + if (!func) { + return this.expect(TokenKind.WORD, "Function not found!")! + } else { + const { genBindings, value } = func(...params) + if (genBindings) this.genBindings.push(...genBindings) + return `(${value})` + } + } + + private expect(kind: TokenKind, err: string) { + const prev = this.at() || this.last() + if (!prev || prev.kind !== kind) { + throw new Error( + `\x1b[31m${this.getPointer(prev)}\n` + `[ERROR]: ${err}\x1b[0m - Expected ${TokenKind[kind]}` + ) + } + } + + private warn(err: string, token?: Token) { + const prev = token || this.at() + console.warn(`\x1b[33m${this.getPointer(prev)}\n` + `[WARNING]: ${err}\x1b[0m`) + } + + private getPointer(token: Token) { + return `${this.input.slice(0, token.start)}>>>${this.input.slice( + token.start, + token.start + token.length + )}<<<${this.input.slice(token.start + token.length)}` + } + + out(): { gen?: BindingItem[]; out: Expression } { + return { + out: this.output, + gen: this.genBindings.map( + ({ source, target }) => + { + binding_type: BindingType.VIEW, + source_property_name: `(${source})`, + target_property_name: `${target}`, + } + ), + } + } +} diff --git a/src/compilers/bindings/index.ts b/src/compilers/bindings/index.ts index 6082dd4..6ef98e5 100644 --- a/src/compilers/bindings/index.ts +++ b/src/compilers/bindings/index.ts @@ -1 +1,4 @@ -export * from "./Bindings.js" +export * from "./Checker.js" +export * from "./Lexer.js" +export * from "./Parser.js" +export * from "./types.js" diff --git a/src/compilers/bindings/types.ts b/src/compilers/bindings/types.ts index 71e8209..3b8fca7 100644 --- a/src/compilers/bindings/types.ts +++ b/src/compilers/bindings/types.ts @@ -12,9 +12,31 @@ export enum TokenKind { COMMA, } +export enum GroupType { + FUNCTION_CALL, + FUNCTION_PARAMS, + OPERATOR_SCOPE, +} + export interface Token { kind: TokenKind value: string start: number length: number } + +export type Expression = string + +export function makeToken(input: string, kind: TokenKind, start: number, length: number = 1): Token { + return { + value: input.slice(start, start + length), + kind, + start, + length, + } +} + +export interface GenBinding { + source: string + target: string +} diff --git a/src/components/Utils.ts b/src/components/Utils.ts index 4767783..30de344 100644 --- a/src/components/Utils.ts +++ b/src/components/Utils.ts @@ -1,5 +1,5 @@ import { Type } from "../types/enums/Type.js" -import { Array3 } from "../types/properties/value.js" +import { Array3, Binding } from "../types/properties/value.js" import { UI } from "./UI.js" import { Renderer } from "../types/enums/Renderer.js" @@ -75,6 +75,10 @@ export function RandomString(length: number, base: number = 32) { return out.join("") } +export function RandomBindingString(length: number, base: number = 32): Binding { + return `#${RandomString(length, base)}` +} + // Quick Elements export function Panel(properties?: Panel, name?: string, namespace?: string) { return new UI(Type.PANEL, name, namespace).setProperties(properties || {}) diff --git a/test/app.ts b/test/app.ts index 7ed14ef..26d63fa 100644 --- a/test/app.ts +++ b/test/app.ts @@ -1,7 +1,5 @@ -import { BindingTranspiler } from ".." +import { Parser } from ".." -const input = "abcdef + 123456" +const { gen, out } = new Parser("new(#a + #b) >= #b").out() -const lexer = BindingTranspiler.lexer(input) - -console.log(lexer) +console.log(gen, out)