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
JavaScript
/**
* 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;