From e53a0e988cccd2b6eb21b5f07be5ffa31ad3378d Mon Sep 17 00:00:00 2001 From: nickp Date: Wed, 25 Oct 2023 19:43:52 +0200 Subject: [PATCH 01/24] Remove logical expression parsing --- .../dimacs/DimacsLogicalExpression.spec.ts | 4 +- .../dimacs/LogicalExpressionParser.ts | 202 +----------------- src/pages/solve/SAT.tsx | 11 +- 3 files changed, 12 insertions(+), 205 deletions(-) diff --git a/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts b/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts index d4d2506..71ecf8b 100644 --- a/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts +++ b/__tests__/converter/dimacs/DimacsLogicalExpression.spec.ts @@ -21,7 +21,7 @@ describe("Parsing", () => { let dimacsParser = new DimacsParser(); let logicalExpressionParser = new LogicalExpressionParser(); - each([ + /*each([ [ "((1 or 2 or not 3) and (!4 and (not 5 and 6)) and 3 and (7 or 2))", "c First comment\nc Some Comment\nc 1 => 1\np sat 7\n*(+( 1 2 -3 )*( -4 *( -5 6 )) 3 +( 7 2 ))", @@ -48,7 +48,7 @@ describe("Parsing", () => { logicalExpression ); } - ); + );*/ each([ [ diff --git a/src/converter/dimacs/LogicalExpressionParser.ts b/src/converter/dimacs/LogicalExpressionParser.ts index e586eaf..7c990b8 100644 --- a/src/converter/dimacs/LogicalExpressionParser.ts +++ b/src/converter/dimacs/LogicalExpressionParser.ts @@ -21,17 +21,9 @@ const rules = [ variableRule, blankRule, ]; -const rulesNegation = [negateRule, variableRule, blankRule]; - -interface LeveledToken { - level: number; - token: Token; -} export class LogicalExpressionParser { private lex: Lexer = new Lexer(rules); - private lexNegation: Lexer = new Lexer(rulesNegation); - private output: string = ""; /** * Converts a logical expression to an expression in dimacs sat format @@ -39,31 +31,12 @@ export class LogicalExpressionParser { * @param logicalExpression {string} logical expression * @returns {string} formula in dimacs sat format of the input */ - public parseDimacs(logicalExpression: string): string { - this.output = ""; - + public validateLogicalExpression(logicalExpression: string): string[] { let tokens = this.lex.tokenize(logicalExpression); - if (tokens.length == 0) return this.output; + if (tokens.length == 0) return []; //Verify formula integrity - let errors = this.verifyTokens(tokens); - if (errors.length > 0) { - let errorStr = ""; - for (let error of errors) errorStr += error + "\n"; - - throw new Error(errorStr); - } - - //Handle variable comments and program start - let variables = this.convertVariables(tokens); - variables.forEach((num, str, _) => { - this.output += `c ${num} ${TokenName.variableAssignment} ${str}\n`; - }); - this.output += `p sat ${variables.size}\n`; - - this.parseFormula(tokens); - - return this.output; + return this.verifyTokens(tokens); } private verifyTokens(tokens: Token[]): string[] { @@ -121,173 +94,4 @@ export class LogicalExpressionParser { return errors; } - /** - * Converts any negative variable's negative prefix - * Converts all variables to numbers - * @param tokens {Token[]} array of tokens to search for variables - * @returns {Map} variables mapped to their number representation - */ - private convertVariables(tokens: Token[]): Map { - let variables = new Map(); - const addToMap = (str: string): number => { - let number = variables.get(str); - if (number == undefined) { - number = variables.size + 1; - variables.set(str, number); - } - - return number; - }; - - for (let i = 0; i < tokens.length; i++) { - switch (tokens[i].name) { - case TokenName.variable: - tokens[i].lexeme = addToMap(tokens[i].lexeme).toString(); - break; - case TokenName.negatedVariable: - let negTokens = this.lexNegation.tokenize(tokens[i].lexeme); - if (negTokens.length != 2) - throw new Error( - "Can't parse negated variables that don't have two tokens" - ); - - let negToken = negTokens[1] as Token; - let varNumber = addToMap(negToken.lexeme); - - //Replace negated variable with dimacs negative variable syntax - negToken.lexeme = `-${varNumber}`; - tokens[i] = negToken; - break; - } - } - - return variables; - } - - private getLeveledTokens(tokens: Token[]): LeveledToken[] { - let leveledTokens: LeveledToken[] = []; - let curLevel = 0; - - for (let token of tokens) { - switch (token.name) { - case TokenName.open: - curLevel++; - break; - case TokenName.close: - curLevel--; - break; - default: - leveledTokens.push({ token: token, level: curLevel }); - break; - } - } - - return leveledTokens; - } - - private parseFormula(tokens: Token[]) { - let leveledTokens = this.getLeveledTokens(tokens); - - //Start recursive parsing of formula starting with or - this.processOR(leveledTokens); - } - - private getOperatorPositions( - tokens: LeveledToken[], - operator: string, - level: number - ): number[] { - let positions: number[] = []; - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - switch (token.token.name) { - case operator: - if (token.level == level) positions.push(i); - break; - default: - break; - } - } - - //Always add token length - positions.push(tokens.length); - - return positions; - } - - private getMinLevel(tokens: LeveledToken[]): number { - let minLevel = undefined; - - for (const token of tokens) { - if (minLevel == undefined || token.level < minLevel) { - minLevel = token.level; - } - } - - return minLevel ?? -1; - } - - private processAND(tokens: LeveledToken[]): LeveledToken[] { - if (tokens.length < 2) { - this.output += ` ${tokens[0].token.lexeme} `; - return tokens; - } - - let level = this.getMinLevel(tokens); - let operatorPositions = this.getOperatorPositions( - tokens, - TokenName.and, - level - ); - if (operatorPositions.length == 1) return this.processOR(tokens); - - let returnTokens: LeveledToken[] = []; - let lastOr = -1; - - this.output += `*(`; - for (let pos of operatorPositions) { - let before = tokens.slice(lastOr + 1, pos); - lastOr = pos; - - let processedBefore = this.processOR(before); - returnTokens.push(...processedBefore); - } - this.output += `)`; - - return returnTokens; - } - - private processOR(tokens: LeveledToken[]): LeveledToken[] { - if (tokens.length < 2) { - this.output += ` ${tokens[0].token.lexeme} `; - return tokens; - } - - let level = this.getMinLevel(tokens); - let operatorPositions = this.getOperatorPositions( - tokens, - TokenName.or, - level - ); - if (operatorPositions.length == 1) return this.processAND(tokens); - - let returnTokens: LeveledToken[] = []; - let lastOr = -1; - - if (operatorPositions.length > 1) this.output += `+(`; - for (let pos of operatorPositions) { - let before = tokens.slice(lastOr + 1, pos); - lastOr = pos; - - //Start processing nested expressions with OR - let processedBefore = - before[0].level != level - ? this.processOR(before) - : this.processAND(before); - returnTokens.push(...processedBefore); - } - if (operatorPositions.length > 1) this.output += `)`; - - return returnTokens; - } } diff --git a/src/pages/solve/SAT.tsx b/src/pages/solve/SAT.tsx index 09d977a..d3db19b 100644 --- a/src/pages/solve/SAT.tsx +++ b/src/pages/solve/SAT.tsx @@ -47,11 +47,14 @@ const SAT: NextPage = () => { setProblemString={(value) => { setLogicalExpressionString(value); - try { - logicalExpressionParser.parseDimacs(value.toString()); + let errors = logicalExpressionParser.validateLogicalExpression( + value.toString() + ); + + if (errors.length > 0) { + setErrorString(errors.toString()); + } else { setErrorString(""); - } catch (e: any) { - setErrorString(e.message); } }} /> From 9341dcd1f5e0c6d5207f3afef38ce7102ba67ca7 Mon Sep 17 00:00:00 2001 From: nickp Date: Wed, 25 Oct 2023 19:45:28 +0200 Subject: [PATCH 02/24] Remove negated variable token and use separate tokens for negate and variable instead --- src/components/solvers/SAT/prism-SAT.ts | 4 ++-- .../dimacs/LogicalExpressionParser.ts | 9 ++------- src/converter/dimacs/Syntax/CommonSyntax.ts | 4 +--- .../dimacs/Syntax/LogicalExpressionSyntax.ts | 18 ++++-------------- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/components/solvers/SAT/prism-SAT.ts b/src/components/solvers/SAT/prism-SAT.ts index ba230f3..0cf8dc9 100644 --- a/src/components/solvers/SAT/prism-SAT.ts +++ b/src/components/solvers/SAT/prism-SAT.ts @@ -1,7 +1,7 @@ import { regexVariable } from "../../../converter/dimacs/Syntax/CommonSyntax"; import { regexAND, - regexNOTVariable, + regexNOT, regexOR, } from "../../../converter/dimacs/Syntax/LogicalExpressionSyntax"; @@ -16,7 +16,7 @@ export const SAT_language = { }, "SAT-punctuation": /[()]/, negation: { - pattern: regexNOTVariable, + pattern: regexNOT, alias: "string", }, "SAT-variable": { diff --git a/src/converter/dimacs/LogicalExpressionParser.ts b/src/converter/dimacs/LogicalExpressionParser.ts index 7c990b8..37ecdd9 100644 --- a/src/converter/dimacs/LogicalExpressionParser.ts +++ b/src/converter/dimacs/LogicalExpressionParser.ts @@ -5,19 +5,14 @@ import { variableRule, blankRule, } from "./Syntax/CommonSyntax"; -import { - andRule, - negatedVariableRule, - negateRule, - orRule, -} from "./Syntax/LogicalExpressionSyntax"; +import { andRule, negateRule, orRule } from "./Syntax/LogicalExpressionSyntax"; // the order is important - tokens are applied from first to last const rules = [ ...parenthesesRule, orRule, andRule, - negatedVariableRule, + negateRule, variableRule, blankRule, ]; diff --git a/src/converter/dimacs/Syntax/CommonSyntax.ts b/src/converter/dimacs/Syntax/CommonSyntax.ts index 50cd601..83f872d 100644 --- a/src/converter/dimacs/Syntax/CommonSyntax.ts +++ b/src/converter/dimacs/Syntax/CommonSyntax.ts @@ -7,12 +7,10 @@ export enum TokenName { open = "open", close = "close", variable = "variable", - negatedVariable = "negatedVariable", variableAssignment = "=>", } -export const variableRegexPart = "(?:[A-z]|\\d)+"; -export const regexVariable = new RegExp(variableRegexPart); +export const regexVariable = /(?:[A-z]|\d)+/; export const regexBlank = /\s+/g; export const variableRule = new Rule({ diff --git a/src/converter/dimacs/Syntax/LogicalExpressionSyntax.ts b/src/converter/dimacs/Syntax/LogicalExpressionSyntax.ts index e600194..ead78af 100644 --- a/src/converter/dimacs/Syntax/LogicalExpressionSyntax.ts +++ b/src/converter/dimacs/Syntax/LogicalExpressionSyntax.ts @@ -1,25 +1,15 @@ import { Rule } from "@jlguenego/lexer"; -import { TokenName, variableRegexPart } from "./CommonSyntax"; +import { TokenName } from "./CommonSyntax"; -export const negateRegexPart = "(?:(?:not|NOT)\\s+|(?:!\\s*))"; - -export const regexAND = /(?:&|and|AND) /; -export const regexOR = /(?:\||or|OR) /; -export const regexNOT = new RegExp(negateRegexPart, "g"); -export const regexNOTVariable = new RegExp( - `${negateRegexPart}\\s*${variableRegexPart}` -); +export const regexAND = /(?:&|and|AND)\b/; +export const regexOR = /(?:\||or|OR)\b/; +export const regexNOT = /not|NOT|!/g; export const negateRule = new Rule({ name: TokenName.negate, pattern: regexNOT, }); -export const negatedVariableRule = new Rule({ - name: TokenName.negatedVariable, - pattern: regexNOTVariable, -}); - export const andRule = new Rule({ name: TokenName.and, pattern: regexAND, From 60085dccf6a4af249c8c6cb7b7e5896ef862e2bf Mon Sep 17 00:00:00 2001 From: nickp Date: Wed, 25 Oct 2023 19:45:44 +0200 Subject: [PATCH 03/24] Add more logical expression validations --- .../dimacs/LogicalExpressionParser.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/converter/dimacs/LogicalExpressionParser.ts b/src/converter/dimacs/LogicalExpressionParser.ts index 37ecdd9..fd36e7f 100644 --- a/src/converter/dimacs/LogicalExpressionParser.ts +++ b/src/converter/dimacs/LogicalExpressionParser.ts @@ -39,6 +39,8 @@ export class LogicalExpressionParser { let wasOperator = false; let wasVariable = false; let wasClose = false; + let wasOpen = false; + let wasNegate = false; if (tokens.length > 0) { switch (tokens[0].name) { @@ -62,8 +64,19 @@ export class LogicalExpressionParser { case TokenName.and: if (wasOperator) errors.push("Two operators (AND/OR) can't be next to each other"); + if (wasOpen) + errors.push( + `Operator '${token.lexeme}' can't be after an opening parenthesis` + ); + break; + case TokenName.negate: + if (wasClose) + errors.push( + `Negation '${token.lexeme}' is after a closing parenthesis` + ); + if (wasVariable) + errors.push(`Negation '${token.lexeme}' is after a variable`); break; - case TokenName.negatedVariable: case TokenName.variable: if (wasVariable) errors.push( @@ -78,13 +91,21 @@ export class LogicalExpressionParser { if (wasVariable) errors.push("Parenthesis can't be opened after a variable"); break; + case TokenName.close: + if (wasOperator) + errors.push( + "Parenthesis can't be closed after an operator (AND/OR)" + ); + if (wasNegate) + errors.push("Parenthesis can't be closed after a negation"); + break; } wasOperator = token.name == TokenName.and || token.name == TokenName.or; - wasVariable = - token.name == TokenName.variable || - token.name == TokenName.negatedVariable; + wasVariable = token.name == TokenName.variable; + wasOpen = token.name == TokenName.open; wasClose = token.name == TokenName.close; + wasNegate = token.name == TokenName.negate; } return errors; From 5d550c875dfa47623d46a69a3bc7eded36706fd6 Mon Sep 17 00:00:00 2001 From: nickp Date: Thu, 2 Nov 2023 19:14:44 +0100 Subject: [PATCH 04/24] Refactor text synchronization --- src/components/solvers/SAT/TextArea.tsx | 2 +- src/components/solvers/TextInputMask.tsx | 11 +++++------ src/pages/solve/FeatureModelAnomaly.tsx | 5 ++--- src/pages/solve/MaxCut.tsx | 3 ++- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/solvers/SAT/TextArea.tsx b/src/components/solvers/SAT/TextArea.tsx index b18ca71..57ebe48 100644 --- a/src/components/solvers/SAT/TextArea.tsx +++ b/src/components/solvers/SAT/TextArea.tsx @@ -7,7 +7,7 @@ import { SAT_language } from "./prism-SAT"; interface TextAreaProps { problemString: string; - setProblemString: React.Dispatch>; + setProblemString: (problemString: string) => void; } export const TextArea = (props: TextAreaProps) => { diff --git a/src/components/solvers/TextInputMask.tsx b/src/components/solvers/TextInputMask.tsx index ec44db7..a4b3329 100644 --- a/src/components/solvers/TextInputMask.tsx +++ b/src/components/solvers/TextInputMask.tsx @@ -8,18 +8,17 @@ import { EditorControls } from "./EditorControls"; export interface TextInputMaskProperties { textPlaceholder: string; - onTextChanged: (text: string) => void; + text: string; + setText: (value: string) => void; body?: ReactElement; } export const TextInputMask = (props: TextInputMaskProperties) => { - const [text, setText] = useState(""); const [errorString, setErrorString] = useState(""); function onTextChanged(text: string): void { try { - setText(text); - props.onTextChanged(text); + props.setText(text); setErrorString(""); } catch (e: any) { @@ -40,11 +39,11 @@ export const TextInputMask = (props: TextInputMaskProperties) => { errorText={errorString} idleText={props.textPlaceholder + " 👇"} onUpload={onTextChanged} - editorContent={text} + editorContent={props.text} />