@@ -51,6 +51,12 @@ class MADXInputWarning(UserWarning):
5151 pass
5252
5353
54+ class _ReturnStatement (Exception ):
55+ """Internal signal that a RETURN statement was encountered in a CALL'd file."""
56+
57+ pass
58+
59+
5460# =============================================================================
5561# Token Types
5662# =============================================================================
@@ -961,13 +967,20 @@ def _parse_statement(self):
961967 self ._parse_variable_assignment (integer = True )
962968 return
963969
970+ if name == "call" :
971+ self ._parse_call_command ()
972+ return
973+
974+ if name == "return" :
975+ # RETURN stops reading the current file (used in CALL'd files)
976+ self ._skip_until_semicolon ()
977+ raise _ReturnStatement ()
978+
964979 if name in (
965980 "title" ,
966981 "option" ,
967982 "select" ,
968983 "twiss" ,
969- "call" ,
970- "return" ,
971984 "print" ,
972985 "value" ,
973986 "show" ,
@@ -1290,6 +1303,91 @@ def _parse_use_command(self):
12901303
12911304 self ._expect (TokenType .SEMICOLON )
12921305
1306+ def _parse_call_command (self ):
1307+ """
1308+ Parse the CALL command.
1309+
1310+ Supports both MAD-X and MAD-8 syntax:
1311+ CALL, FILE="other_file";
1312+ CALL, FILENAME="other_file";
1313+
1314+ The called file is read and parsed. If the called file contains a
1315+ RETURN statement, parsing stops at that point. Called files may
1316+ themselves contain CALL statements to any depth.
1317+ """
1318+ self ._advance () # skip 'call'
1319+
1320+ filename = None
1321+
1322+ while self ._current ().type == TokenType .COMMA :
1323+ self ._advance () # skip comma
1324+
1325+ if self ._current ().type != TokenType .IDENTIFIER :
1326+ break
1327+
1328+ attr_name = self ._advance ().value .lower ()
1329+
1330+ if attr_name in ("file" , "filename" ):
1331+ self ._expect (TokenType .EQUALS )
1332+ if self ._current ().type == TokenType .STRING :
1333+ filename = self ._advance ().value
1334+ elif self ._current ().type == TokenType .IDENTIFIER :
1335+ # Some MAD-X files use unquoted filenames
1336+ filename = self ._advance ().value
1337+ else :
1338+ raise MADXInputError (
1339+ "Expected filename after FILE=" , self ._current ().line
1340+ )
1341+ else :
1342+ # Unknown attribute, skip its value
1343+ if self ._current ().type == TokenType .EQUALS :
1344+ self ._advance ()
1345+ self ._parse_expression ()
1346+
1347+ self ._expect (TokenType .SEMICOLON )
1348+
1349+ if filename is None :
1350+ raise MADXInputError ("CALL command requires FILE= or FILENAME= attribute" )
1351+
1352+ # Resolve the filename relative to the current file's directory
1353+ if self ._current_file and self ._current_file != "<string>" :
1354+ base_dir = os .path .dirname (os .path .abspath (self ._current_file ))
1355+ resolved = os .path .join (base_dir , filename )
1356+ else :
1357+ resolved = filename
1358+
1359+ if not os .path .isfile (resolved ):
1360+ raise FileNotFoundError (
1361+ f"CALL: File '{ filename } ' not found (resolved to '{ resolved } ')"
1362+ )
1363+
1364+ # Save parser state
1365+ saved_tokens = self .tokens
1366+ saved_pos = self .pos
1367+ saved_file = self ._current_file
1368+
1369+ try :
1370+ # Parse the called file
1371+ self ._current_file = resolved
1372+ with open (resolved , "r" ) as f :
1373+ text = f .read ()
1374+
1375+ lexer = MADXLexer (text )
1376+ self .tokens = lexer .tokenize ()
1377+ self .pos = 0
1378+
1379+ while self ._current ().type != TokenType .EOF :
1380+ self ._parse_statement ()
1381+ self ._skip_semicolons ()
1382+ except _ReturnStatement :
1383+ # RETURN was encountered in the called file — stop reading it
1384+ pass
1385+ finally :
1386+ # Restore parser state
1387+ self .tokens = saved_tokens
1388+ self .pos = saved_pos
1389+ self ._current_file = saved_file
1390+
12931391 # =========================================================================
12941392 # Public Interface (backward compatible with old parser)
12951393 # =========================================================================
0 commit comments