UNPKG

uppaal-to-tchecker

Version:

JavaScript implementation of Uppaal to TChecker translator - convert Uppaal timed automata models to TChecker format

1,282 lines (1,128 loc) 44.9 kB
/** * Uppaal Parser * Simplified JavaScript implementation for parsing Uppaal XTA/XML files */ const xml2js = require('xml2js'); const { ParserException } = require('./exceptions'); class UppaalParser { constructor() { this.xmlParser = new xml2js.Parser({ explicitArray: false, mergeAttrs: true, ignoreAttrs: false }); } /** * Parse Uppaal file content * @param {string} content - File content * @param {string} format - 'xml' or 'xta' * @param {string} language - 'xta' or 'ta' * @returns {Object} - Parsed timed automata system */ async parse(content, format = 'xml', language = 'xta') { if (format === 'xml') { return this.parseXML(content, language === 'xta'); } else { return this.parseXTA(content, language === 'xta'); } } /** * Parse XML format * @param {string} content - XML content * @param {boolean} isXTA - Whether it's XTA language * @returns {Object} - Parsed system */ async parseXML(content, isXTA = true) { try { const result = await this.xmlParser.parseStringPromise(content); return this.convertXMLToSystem(result, isXTA); } catch (error) { throw new ParserException(`XML parsing error: ${error.message}`); } } /** * Parse XTA format * @param {string} content - XTA content * @param {boolean} isXTA - Whether it's XTA language * @returns {Object} - Parsed system */ parseXTA(content, isXTA = true) { try { return this.parseXTAContent(content, isXTA); } catch (error) { throw new ParserException(`XTA parsing error: ${error.message}`); } } /** * Convert XML structure to internal system representation * @param {Object} xmlResult - Parsed XML * @param {boolean} isXTA - Whether it's XTA language * @returns {Object} - System representation */ convertXMLToSystem(xmlResult, isXTA) { const nta = xmlResult.nta; if (!nta) { throw new ParserException('Invalid Uppaal XML: missing nta element'); } const system = { declarations: this.parseDeclarations(nta.declaration || ''), templates: [], system: nta.system || '', errors: [], warnings: [] }; // Parse templates if (nta.template) { const templates = Array.isArray(nta.template) ? nta.template : [nta.template]; for (const template of templates) { system.templates.push(this.parseTemplate(template)); } } return system; } /** * Parse XTA content * @param {string} content - XTA content * @param {boolean} isXTA - Whether it's XTA language * @returns {Object} - System representation */ parseXTAContent(content, isXTA) { const system = { declarations: [], templates: [], instances: [], // 新增:模板实例化 system: '', errors: [], warnings: [] }; // Simple XTA parser - split by sections const sections = this.splitXTASections(content); // Parse global declarations and template instances if (sections.declarations) { const { declarations, instances } = this.parseDeclarationsAndInstances(sections.declarations); system.declarations = declarations; system.instances = instances; } // Parse templates/processes for (const [name, templateContent] of Object.entries(sections.templates)) { system.templates.push(this.parseXTATemplate(name, templateContent)); } // Parse system declaration if (sections.system) { system.system = sections.system; } return system; } /** * Split XTA content into sections * @param {string} content - XTA content * @returns {Object} - Sections */ splitXTASections(content) { const sections = { declarations: '', templates: {}, system: '' }; // Remove comments const cleanContent = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''); // Simple section parsing - this is a simplified version const lines = cleanContent.split('\n'); let currentSection = 'declarations'; let currentTemplate = null; let currentContent = []; let declarationLines = []; // Collect all declaration lines for (let line of lines) { line = line.trim(); if (!line) continue; // Check for process or template declaration const processMatch = line.match(/^process\s+(\w+)\s*\(/); const templateMatch = line.match(/^template\s+(\w+)\s*\(/); if (processMatch || templateMatch) { // 保存之前的section if (currentSection === 'declarations') { declarationLines.push(...currentContent); } else if (currentTemplate) { sections.templates[currentTemplate] = currentContent.join('\n'); } currentTemplate = processMatch ? processMatch[1] : templateMatch[1]; currentContent = [line]; currentSection = 'template'; continue; } // Check for system declaration if (line.startsWith('system')) { // 保存之前的section if (currentSection === 'declarations') { declarationLines.push(...currentContent); } else if (currentTemplate) { sections.templates[currentTemplate] = currentContent.join('\n'); currentTemplate = null; } currentSection = 'system'; currentContent = [line]; continue; } // Check for template instantiation (C1 = Template(args);) if (line.match(/^\w+\s*=\s*\w+\s*\([^)]*\)\s*;?$/)) { // This is a template instantiation, treat as declaration if (currentSection === 'template' && currentTemplate) { // Save current template first sections.templates[currentTemplate] = currentContent.join('\n'); currentTemplate = null; } declarationLines.push(line); currentSection = 'declarations'; currentContent = []; continue; } currentContent.push(line); } // Handle final section if (currentSection === 'system') { sections.system = currentContent.join('\n'); } else if (currentSection === 'template' && currentTemplate) { sections.templates[currentTemplate] = currentContent.join('\n'); } else if (currentSection === 'declarations') { declarationLines.push(...currentContent); } // Set declarations section sections.declarations = declarationLines.join('\n'); return sections; } /** * Parse template from XML * @param {Object} template - XML template * @returns {Object} - Parsed template */ parseTemplate(template) { const result = { name: template.name || 'UnnamedTemplate', parameters: this.parseParameters(template.parameter), declarations: this.parseDeclarations(template.declaration || ''), locations: [], transitions: [], init: null }; // Parse locations if (template.location) { const locations = Array.isArray(template.location) ? template.location : [template.location]; for (const loc of locations) { const location = this.parseLocation(loc); result.locations.push(location); if (location.id === template.init) { result.init = location.id; } } } // Parse transitions if (template.transition) { const transitions = Array.isArray(template.transition) ? template.transition : [template.transition]; for (const trans of transitions) { result.transitions.push(this.parseTransition(trans)); } } return result; } /** * Parse template parameters from header * @param {string} content - Template content * @returns {Array} - Array of parameter objects */ parseTemplateParameters(content) { const lines = content.split('\n'); const headerLine = lines[0]; // template name(params) { const paramMatch = headerLine.match(/template\s+\w+\s*\(([^)]*)\)/); if (!paramMatch || !paramMatch[1].trim()) return []; const paramString = paramMatch[1].trim(); const paramDecls = paramString.split(',').map(p => p.trim()); return paramDecls.map(decl => { // Parse parameter like "const int max_val" const parts = decl.split(/\s+/); const isConst = parts[0] === 'const'; const type = isConst ? parts[1] : parts[0]; const name = isConst ? parts[2] : parts[1]; return { name: name, type: type, isConst: isConst }; }); } /** * Parse XTA template * @param {string} name - Template name * @param {string} content - Template content * @returns {Object} - Parsed template */ parseXTATemplate(name, content) { // Extract parameters from template header const params = this.parseTemplateParameters(content); const result = { name: name, parameters: params, declarations: [], locations: [], transitions: [], init: null }; // Parse multi-line transitions properly content = this.normalizeXTAContent(content); const lines = content.split('\n').map(line => line.trim()).filter(line => line); for (const line of lines) { if (line.includes('state') || line.includes('location')) { this.parseXTALocations(line, result); } else if (line.includes('trans') && line.includes('->')) { // 处理单行和多行转换 this.parseXTAMultiTransitions(line, result); } else if (line.includes('init')) { const initMatch = line.match(/init\s+(\w+)/); if (initMatch) { result.init = initMatch[1]; } } } return result; } /** * Normalize XTA content to handle multi-line transitions and state declarations * @param {string} content - XTA template content * @returns {string} - Normalized content */ normalizeXTAContent(content) { // 将多行转换和状态声明合并为单行 let lines = content.split('\n'); let result = []; let i = 0; while (i < lines.length) { let line = lines[i].trim(); // Check if this line starts a trans statement if (line.startsWith('trans ')) { let transStatement = line; let braceDepth = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; i++; // Continue collecting lines until we find the semicolon AND braces are balanced while (i < lines.length && (!transStatement.endsWith(';') || braceDepth > 0)) { let nextLine = lines[i].trim(); if (nextLine) { transStatement += ' ' + nextLine; // Update brace depth braceDepth += (nextLine.match(/\{/g) || []).length - (nextLine.match(/\}/g) || []).length; } i++; } result.push(transStatement); } else if (line.startsWith('state ')) { // Handle multi-line state declarations let stateStatement = line; i++; // Continue collecting lines until we find the semicolon while (i < lines.length && !stateStatement.endsWith(';')) { let nextLine = lines[i].trim(); if (nextLine) { // Remove comments before adding nextLine = nextLine.replace(/\/\/.*$/, '').trim(); if (nextLine) { stateStatement += ' ' + nextLine; } } i++; } result.push(stateStatement); } else { result.push(line); i++; } } return result.join('\n'); } /** * Parse multiple transitions from a single trans statement * @param {string} line - Line containing transitions * @param {Object} result - Template result to populate */ parseXTAMultiTransitions(line, result) { // 处理形如 trans s0 -> s1 { ... }, s1 -> s2 { ... }; 的多转换 const transPrefix = line.match(/^trans\s+/); if (!transPrefix) return; const transContent = line.replace(/^trans\s+/, '').replace(/;$/, ''); const transitions = this.splitTransitions(transContent); for (const trans of transitions) { this.parseXTATransition(trans.trim(), result); } } /** * Split transition string into individual transitions * @param {string} content - Transitions content * @returns {Array} - Array of individual transitions */ splitTransitions(content) { const transitions = []; let current = ''; let braceDepth = 0; let i = 0; while (i < content.length) { const char = content[i]; if (char === '{') { braceDepth++; } else if (char === '}') { braceDepth--; } if (char === ',' && braceDepth === 0) { // Found a transition separator if (current.trim()) { transitions.push(current.trim()); } current = ''; } else { current += char; } i++; } // Add the last transition if (current.trim()) { transitions.push(current.trim()); } return transitions; } /** * Parse XTA locations line * @param {string} line - Line containing locations * @param {Object} result - Template result to populate */ parseXTALocations(line, result) { // Enhanced location parsing to handle individual location attributes // Parse state declarations like: state s0 { x <= 2 }, s1, s2 { committed }, s3 { urgent }; // First, remove the 'state' keyword const stateContent = line.replace(/^state\s+/, '').replace(/;$/, ''); // Split by commas but respect braces const locationDefs = this.splitLocationDefinitions(stateContent); for (const locDef of locationDefs) { const location = this.parseLocationDefinition(locDef.trim()); if (location) { result.locations.push(location); } } } /** * Split location definitions respecting braces * @param {string} content - Location definitions content * @returns {Array} - Array of location definitions */ splitLocationDefinitions(content) { const definitions = []; let current = ''; let braceDepth = 0; let i = 0; while (i < content.length) { const char = content[i]; if (char === '{') { braceDepth++; } else if (char === '}') { braceDepth--; } if (char === ',' && braceDepth === 0) { // Found a location separator if (current.trim()) { definitions.push(current.trim()); } current = ''; } else { current += char; } i++; } // Add the last definition if (current.trim()) { definitions.push(current.trim()); } return definitions; } /** * Parse individual location definition * @param {string} locDef - Location definition like "s0 { x <= 2 }" or "s1" or "s2 { committed }" * @returns {Object|null} - Parsed location object */ parseLocationDefinition(locDef) { // Match patterns like: s0, s1 { condition }, s2 { committed }, s3 { urgent } const match = locDef.match(/^(\w+)(?:\s*\{([^}]*)\})?$/); if (!match) return null; const locName = match[1]; const attributes = match[2] ? match[2].trim() : ''; const location = { id: locName, name: locName, invariant: null, urgent: false, committed: false }; // Parse attributes inside braces if (attributes) { if (attributes === 'committed') { location.committed = true; } else if (attributes === 'urgent') { location.urgent = true; } else { // Assume it's an invariant condition location.invariant = this.parseExpression(attributes); } } return location; } /** * Parse XTA transition line * @param {string} line - Line containing transition * @param {Object} result - Template result to populate */ parseXTATransition(line, result) { // Parse transition: s0 -> s1 { guard; sync; assign; } (with or without 'trans' prefix) const transMatch = line.match(/(?:trans\s+)?(\w+)\s*->\s*(\w+)(?:\s*\{([^}]*)\})?/); if (transMatch) { const source = transMatch[1]; const target = transMatch[2]; const actions = transMatch[3] ? transMatch[3].trim() : ''; const transition = { source: source, target: target, guard: null, sync: null, update: null }; if (actions) { this.parseTransitionActions(actions, transition); } result.transitions.push(transition); } } /** * Parse transition actions * @param {string} actions - Actions string * @param {Object} transition - Transition to populate */ parseTransitionActions(actions, transition) { const parts = actions.split(';').map(part => part.trim()).filter(part => part); for (const part of parts) { if (part.startsWith('guard ')) { const guardMatch = part.match(/guard\s+(.+)/); if (guardMatch) { transition.guard = this.parseExpression(guardMatch[1]); } } else if (part.startsWith('sync ')) { const syncMatch = part.match(/sync\s+(.+)/); if (syncMatch) { transition.sync = this.parseSync(syncMatch[1]); } } else if (part.startsWith('assign ')) { const assignMatch = part.match(/assign\s+(.+)/); if (assignMatch) { // 处理多变量赋值: assign x = 0, n = 1 transition.update = this.parseMultipleAssignments(assignMatch[1]); } } else if (part.includes('=') && !part.includes('==') && !part.includes('!=') && !part.includes('<=') && !part.includes('>=')) { // Direct assignment - 可能是多变量赋值 transition.update = this.parseMultipleAssignments(part); } else if (part.includes('!') || part.includes('?')) { // Direct synchronization transition.sync = this.parseSync(part); } else { // Assume it's a guard (比较表达式等) transition.guard = this.parseExpression(part); } } } /** * Parse multiple assignments (e.g., "x = 0, n = 1") * @param {string} assignments - Assignment string * @returns {Object} - Parsed assignments */ parseMultipleAssignments(assignments) { const assignmentList = assignments.split(',').map(assignment => assignment.trim()); if (assignmentList.length === 1) { // Single assignment return this.parseExpression(assignments); } // Multiple assignments - create a compound assignment object const parsedAssignments = assignmentList.map(assignment => { return this.parseExpression(assignment); }); return { kind: 'MULTIPLE_ASSIGN', assignments: parsedAssignments }; } /** * Parse declarations and template instances * @param {string} declarations - Declaration string * @returns {Object} - Object with declarations and instances arrays */ parseDeclarationsAndInstances(declarations) { if (!declarations) return { declarations: [], instances: [] }; const declResult = []; const instanceResult = []; const lines = declarations.split(';').map(line => line.trim()).filter(line => line); for (const line of lines) { // Check for template instantiation: InstanceName = TemplateName(params) const instanceMatch = line.match(/^(\w+)\s*=\s*(\w+)\s*\(([^)]*)\)$/); if (instanceMatch) { const instanceName = instanceMatch[1]; const templateName = instanceMatch[2]; const params = instanceMatch[3].trim(); // Parse parameters const paramList = params ? params.split(',').map(p => p.trim()) : []; instanceResult.push({ name: instanceName, template: templateName, parameters: paramList }); continue; } // Regular declaration parsing const decl = this.parseDeclaration(line); if (decl) { // parseDeclaration可能返回数组(如clock x, y;),需要展开 if (Array.isArray(decl)) { declResult.push(...decl); } else { declResult.push(decl); } } } return { declarations: declResult, instances: instanceResult }; } /** * Parse declarations * @param {string} declarations - Declaration string * @returns {Array} - Array of declarations */ parseDeclarations(declarations) { const result = this.parseDeclarationsAndInstances(declarations); return result.declarations; } /** * Split declaration list respecting brackets and braces for arrays * @param {string} declarations - Declaration list string * @returns {Array} - Array of individual declarations */ splitDeclarationList(declarations) { const result = []; let current = ''; let bracketDepth = 0; let braceDepth = 0; let i = 0; while (i < declarations.length) { const char = declarations[i]; if (char === '[') { bracketDepth++; } else if (char === ']') { bracketDepth--; } else if (char === '{') { braceDepth++; } else if (char === '}') { braceDepth--; } if (char === ',' && bracketDepth === 0 && braceDepth === 0) { // Found a declaration separator outside of brackets and braces if (current.trim()) { result.push(current.trim()); } current = ''; } else { current += char; } i++; } // Add the last declaration if (current.trim()) { result.push(current.trim()); } return result; } /** * Parse single declaration * @param {string} line - Declaration line * @returns {Object|null} - Parsed declaration */ parseDeclaration(line) { // 常量声明: const int NAME = value; const constMatch = line.match(/const\s+(int|bool)\s+(\w+)\s*=\s*(.+)/); if (constMatch) { const type = constMatch[1]; const name = constMatch[2]; const value = constMatch[3].trim(); let constValue; if (type === 'bool') { constValue = value === 'true' ? 1 : 0; } else { constValue = parseInt(value); } return { kind: 'CONSTANT', name: name, type: { kind: type.toUpperCase(), isInteger: type === 'int', isBoolean: type === 'bool' }, value: constValue }; } // Clock declaration: clock x, y; 或 clock x[5]; const clockMatch = line.match(/clock\s+([^;]+)/); if (clockMatch) { const declarations = this.splitDeclarationList(clockMatch[1]); return declarations.map(decl => { const arrayMatch = decl.match(/(\w+)\[(\d+)\]/); if (arrayMatch) { return { kind: 'CLOCK', name: arrayMatch[1], size: parseInt(arrayMatch[2]) }; } else { return { kind: 'CLOCK', name: decl, size: 1 }; } }); } // 有界整数声明: int[0,10] x = 5; 或 int[0,10] arr[5] = {1,2,3}; const boundedIntMatch = line.match(/int\[([^,\]]+),([^\]]+)\]\s+([^;=]+)(?:\s*=\s*([^;]+))?/); if (boundedIntMatch) { const minVal = parseInt(boundedIntMatch[1].trim()); const maxVal = parseInt(boundedIntMatch[2].trim()); const initValue = boundedIntMatch[4] ? boundedIntMatch[4].trim() : null; let initVal = null; if (initValue) { // 处理数组字面量 {v1,v2,v3} if (initValue.startsWith('{') && initValue.endsWith('}')) { const arrayValues = initValue.slice(1, -1).split(',').map(v => parseInt(v.trim())); // 对于数组初始化,使用第一个值或默认值 initVal = arrayValues.length > 0 ? arrayValues[0] : 0; } else { initVal = parseInt(initValue); } } const declarations = this.splitDeclarationList(boundedIntMatch[3]); return declarations.map(decl => { // Check if this is an array declaration const arrayMatch = decl.match(/(\w+)\[([^\]]+)\]/); if (arrayMatch) { const name = arrayMatch[1]; const dimensions = arrayMatch[2]; const dims = dimensions.split(',').map(d => parseInt(d.trim())); return { kind: 'ARRAY', name: name, dimensions: dims, size: dims.reduce((a, b) => a * b, 1), type: { kind: 'RANGE', isInteger: true, range: { min: minVal, max: maxVal } }, init: initVal }; } else { // Simple bounded variable return { kind: 'VARIABLE', name: decl, type: { kind: 'RANGE', isInteger: true, range: { min: minVal, max: maxVal } }, init: initVal }; } }); } // 一般整数声明: int x, y = 10; 或 int arr[5]; 或 int matrix[2,3]; const intMatch = line.match(/int\s+([^;]+)/); if (intMatch) { const declarations = this.splitDeclarationList(intMatch[1]); return declarations.map(decl => { // 匹配带初始值的声明: name[dims] = value 或 name[dims] = {v1,v2,v3} const initMatch = decl.match(/(\w+)(?:\[([^\]]+)\])?\s*=\s*(.+)/); if (initMatch) { const name = initMatch[1]; const dimensions = initMatch[2]; const initValue = initMatch[3].trim(); let initVal; // 处理数组字面量 {v1,v2,v3} if (initValue.startsWith('{') && initValue.endsWith('}')) { const arrayValues = initValue.slice(1, -1).split(',').map(v => parseInt(v.trim())); // 对于数组初始化,使用第一个值或默认值 initVal = arrayValues.length > 0 ? arrayValues[0] : 0; } else { initVal = parseInt(initValue); } if (dimensions) { const dims = dimensions.split(',').map(d => parseInt(d.trim())); return { kind: 'ARRAY', name: name, dimensions: dims, size: dims.reduce((a, b) => a * b, 1), type: { kind: 'INT', isInteger: true }, init: initVal }; } else { return { kind: 'VARIABLE', name: name, type: { kind: 'INT', isInteger: true }, init: initVal }; } } else { // 匹配数组声明: name[dims] const arrayMatch = decl.match(/(\w+)\[([^\]]+)\]/); if (arrayMatch) { const name = arrayMatch[1]; const dimensions = arrayMatch[2]; const dims = dimensions.split(',').map(d => parseInt(d.trim())); return { kind: 'ARRAY', name: name, dimensions: dims, size: dims.reduce((a, b) => a * b, 1), type: { kind: 'INT', isInteger: true }, init: null }; } else { // 简单变量声明 return { kind: 'VARIABLE', name: decl, type: { kind: 'INT', isInteger: true }, init: null }; } } }); } // Bool declaration: bool flag = true; 或 bool flags[5]; const boolMatch = line.match(/bool\s+([^;]+)/); if (boolMatch) { const declarations = this.splitDeclarationList(boolMatch[1]); return declarations.map(decl => { // Check for initialization first const initMatch = decl.match(/(\w+)(?:\[([^\]]+)\])?\s*=\s*(.+)/); if (initMatch) { const name = initMatch[1]; const dimensions = initMatch[2]; const initValue = initMatch[3].trim(); let initVal; // 处理数组字面量 {true,false,true} if (initValue.startsWith('{') && initValue.endsWith('}')) { const arrayValues = initValue.slice(1, -1).split(',').map(v => { const val = v.trim(); return val === 'true' ? 1 : 0; }); // 对于数组初始化,使用第一个值或默认值 initVal = arrayValues.length > 0 ? arrayValues[0] : 0; } else { initVal = initValue === 'true' ? 1 : 0; } if (dimensions) { const dims = dimensions.split(',').map(d => parseInt(d.trim())); return { kind: 'ARRAY', name: name, dimensions: dims, size: dims.reduce((a, b) => a * b, 1), type: { kind: 'BOOL', isBoolean: true }, init: initVal }; } else { return { kind: 'VARIABLE', name: name, type: { kind: 'BOOL', isBoolean: true }, init: initVal }; } } else { // Check if this is an array declaration const arrayMatch = decl.match(/(\w+)\[([^\]]+)\]/); if (arrayMatch) { const name = arrayMatch[1]; const dimensions = arrayMatch[2]; const dims = dimensions.split(',').map(d => parseInt(d.trim())); return { kind: 'ARRAY', name: name, dimensions: dims, size: dims.reduce((a, b) => a * b, 1), type: { kind: 'BOOL', isBoolean: true }, init: null }; } else { return { kind: 'VARIABLE', name: decl, type: { kind: 'BOOL', isBoolean: true }, init: null }; } } }); } // Channel declaration: chan a, b; 或 broadcast chan alarm; const chanMatch = line.match(/(broadcast\s+)?chan\s+([^;]+)/); if (chanMatch) { const isBroadcast = !!chanMatch[1]; const names = this.splitDeclarationList(chanMatch[2]); return names.map(name => ({ kind: 'CHANNEL', name: name, broadcast: isBroadcast })); } return null; } /** * Parse location from XML * @param {Object} loc - XML location * @returns {Object} - Parsed location */ parseLocation(loc) { return { id: loc.id, name: loc.name || loc.id, invariant: loc.label && loc.label.kind === 'invariant' ? this.parseExpression(loc.label._) : null, urgent: loc.urgent === 'true', committed: loc.committed === 'true' }; } /** * Parse transition from XML * @param {Object} trans - XML transition * @returns {Object} - Parsed transition */ parseTransition(trans) { const result = { source: trans.source, target: trans.target, guard: null, sync: null, update: null }; if (trans.label) { const labels = Array.isArray(trans.label) ? trans.label : [trans.label]; for (const label of labels) { switch (label.kind) { case 'guard': result.guard = this.parseExpression(label._); break; case 'synchronisation': result.sync = this.parseSync(label._); break; case 'assignment': result.update = this.parseExpression(label._); break; } } } return result; } /** * Parse expression (enhanced to support complex expressions) * @param {string} expr - Expression string * @returns {Object} - Parsed expression */ parseExpression(expr) { if (!expr || typeof expr !== 'string') return null; expr = expr.trim(); // Number constant (including negative numbers) if (/^-?\d+$/.test(expr)) { return { kind: 'CONSTANT', value: parseInt(expr) }; } // Boolean constant if (expr === 'true') { return { kind: 'CONSTANT', value: 1 }; } if (expr === 'false') { return { kind: 'CONSTANT', value: 0 }; } // Handle parentheses if (expr.startsWith('(') && expr.endsWith(')')) { const inner = expr.slice(1, -1); return this.parseExpression(inner); } // Unary operators if (expr.startsWith('!')) { return { kind: 'NOT', operand: this.parseExpression(expr.slice(1)) }; } if (expr.startsWith('-') && expr.length > 1 && !(/^\-?\d+$/.test(expr))) { return { kind: 'UNARY_MINUS', operand: this.parseExpression(expr.slice(1)) }; } // Assignment (but not comparison operators) if (expr.includes('=') && !expr.includes('==') && !expr.includes('!=') && !expr.includes('<=') && !expr.includes('>=')) { const assignIndex = expr.indexOf('='); return { kind: 'ASSIGN', left: this.parseExpression(expr.substring(0, assignIndex).trim()), right: this.parseExpression(expr.substring(assignIndex + 1).trim()) }; } // Binary operators (sorted by precedence - lowest first) const binaryOps = [ // Logical OR ['||', 'OR'], // Logical AND ['&&', 'AND'], // Equality ['==', 'EQ'], ['!=', 'NEQ'], // Relational ['<=', 'LE'], ['>=', 'GE'], ['<', 'LT'], ['>', 'GT'], // Additive ['+', 'PLUS'], ['-', 'MINUS'], // Multiplicative ['*', 'MULT'], ['/', 'DIV'], ['%', 'MOD'] ]; for (const [op, kind] of binaryOps) { const opIndex = this.findOperatorIndex(expr, op); if (opIndex !== -1) { return { kind: kind, left: this.parseExpression(expr.substring(0, opIndex).trim()), right: this.parseExpression(expr.substring(opIndex + op.length).trim()) }; } } // Array access: arr[index] const arrayMatch = expr.match(/(\w+)\[([^\]]+)\]/); if (arrayMatch) { return { kind: 'ARRAY_ACCESS', base: { kind: 'IDENTIFIER', symbol: { name: arrayMatch[1] } }, index: this.parseExpression(arrayMatch[2]) }; } // Simple identifier if (/^\w+$/.test(expr)) { return { kind: 'IDENTIFIER', symbol: { name: expr } }; } // Default: treat as identifier return { kind: 'IDENTIFIER', symbol: { name: expr } }; } /** * Find operator index, respecting parentheses * @param {string} expr - Expression string * @param {string} op - Operator to find * @returns {number} - Index of operator, -1 if not found */ findOperatorIndex(expr, op) { let depth = 0; let i = 0; while (i <= expr.length - op.length) { const char = expr[i]; if (char === '(') { depth++; } else if (char === ')') { depth--; } else if (depth === 0 && expr.substring(i, i + op.length) === op) { // Make sure it's not part of another operator (e.g., >= when looking for >) const prevChar = i > 0 ? expr[i - 1] : ''; const nextChar = i + op.length < expr.length ? expr[i + op.length] : ''; if (op === '<' && nextChar === '=') continue; if (op === '>' && nextChar === '=') continue; if (op === '=' && nextChar === '=') continue; if (op === '!' && nextChar === '=') continue; if (op === '=' && prevChar === '!') continue; if (op === '=' && prevChar === '<') continue; if (op === '=' && prevChar === '>') continue; return i; } i++; } return -1; } /** * Parse synchronization * @param {string} sync - Sync string * @returns {Object} - Parsed sync */ parseSync(sync) { if (!sync) return null; sync = sync.trim(); if (sync.endsWith('!')) { return { kind: 'SEND', channel: sync.slice(0, -1), broadcast: false }; } if (sync.endsWith('?')) { return { kind: 'RECEIVE', channel: sync.slice(0, -1) }; } return { kind: 'CSP', channel: sync }; } /** * Parse parameters * @param {string} params - Parameter string * @returns {Array} - Array of parameters */ parseParameters(params) { if (!params) return []; // Simple parameter parsing return params.split(',').map(param => { const trimmed = param.trim(); const parts = trimmed.split(/\s+/); return { type: parts[0], name: parts[parts.length - 1] }; }); } /** * Get operator kind from string * @param {string} op - Operator string * @returns {string} - Operator kind */ getOperatorKind(op) { const opMap = { '+': 'PLUS', '-': 'MINUS', '*': 'MULT', '/': 'DIV', '%': 'MOD', '<': 'LT', '<=': 'LE', '>': 'GT', '>=': 'GE', '==': 'EQ', '!=': 'NEQ', '&&': 'AND', '||': 'OR' }; return opMap[op] || 'UNKNOWN'; } } module.exports = UppaalParser;