This commit is contained in:
Asaki Yuki 2026-01-17 02:03:47 +07:00
parent 4d5f81f7e5
commit df7c642077
10 changed files with 440 additions and 157 deletions

View file

@ -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[]) {},
}

View file

@ -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)
}

View file

@ -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",
}
})

View file

@ -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
}

View file

@ -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 }) =>
<BindingItem>{
binding_type: BindingType.VIEW,
source_property_name: `(${source})`,
target_property_name: `${target}`,
}
),
}
}
}

View file

@ -1 +1,4 @@
export * from "./Bindings.js"
export * from "./Checker.js"
export * from "./Lexer.js"
export * from "./Parser.js"
export * from "./types.js"

View file

@ -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
}