check_board_header.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (c) 2024 Raspberry Pi Ltd.
  4. #
  5. # SPDX-License-Identifier: BSD-3-Clause
  6. #
  7. #
  8. # Simple script to check basic validity of a board-header-file
  9. #
  10. # Usage:
  11. #
  12. # tools/check_board_header.py src/boards/include/boards/<board.h>
  13. import re
  14. import sys
  15. import os.path
  16. import json
  17. import warnings
  18. from collections import namedtuple
  19. # warnings off by default, because some boards use the same pin for multiple purposes
  20. show_warnings = False
  21. chip_interfaces = {
  22. 'RP2040': "src/rp2040/rp2040_interface_pins.json",
  23. 'RP2350A': "src/rp2350/rp2350a_interface_pins.json",
  24. 'RP2350B': "src/rp2350/rp2350b_interface_pins.json",
  25. }
  26. compulsory_cmake_settings = set(['PICO_PLATFORM'])
  27. compulsory_cmake_default_settings = set(['PICO_FLASH_SIZE_BYTES'])
  28. matching_cmake_default_settings = set(['PICO_FLASH_SIZE_BYTES', 'PICO_RP2350_A2_SUPPORTED'])
  29. compulsory_defines = set(['PICO_FLASH_SIZE_BYTES'])
  30. DefineType = namedtuple("DefineType", ["name", "value", "resolved_value", "lineno"])
  31. def list_to_string_with(lst, joiner):
  32. elems = len(lst)
  33. if elems == 0:
  34. return ""
  35. elif elems == 1:
  36. return str(lst[0])
  37. else:
  38. return "{} {} {}".format(", ".join(str(l) for l in lst[:-1]), joiner, lst[-1])
  39. board_header = sys.argv[1]
  40. if not os.path.isfile(board_header):
  41. raise Exception("{} doesn't exist".format(board_header))
  42. board_header_basename = os.path.basename(board_header)
  43. expected_include_suggestion = "/".join(board_header.split("/")[-2:])
  44. expected_include_guard = "_" + re.sub(r"\W", "_", expected_include_suggestion.upper())
  45. expected_board_detection = re.sub(r"\W", "_", expected_include_suggestion.split("/")[-1].upper()[:-2])
  46. defines = dict()
  47. cmake_settings = dict()
  48. cmake_default_settings = dict()
  49. has_include_guard = False
  50. has_board_detection = False
  51. has_include_suggestion = False
  52. def read_defines_from(header_file, defines_dict):
  53. with open(header_file) as fh:
  54. last_ifndef = None
  55. last_ifndef_lineno = -1
  56. validity_stack = [True]
  57. board_detection_is_next = False
  58. for lineno, line in enumerate(fh.readlines()):
  59. lineno += 1
  60. # strip trailing comments
  61. line = re.sub(r"(?<=\S)\s*//.*$", "", line)
  62. # look for "// pico_cmake_set BLAH_BLAH=42"
  63. m = re.match(r"^\s*//\s*pico_cmake_set\s+(\w+)\s*=\s*(.+?)\s*$", line)
  64. if m:
  65. #print(m.groups())
  66. name = m.group(1)
  67. value = m.group(2)
  68. # check all uppercase
  69. if name != name.upper():
  70. raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
  71. # check for multiply-defined values
  72. if name in cmake_settings:
  73. if cmake_settings[name].value != value:
  74. raise Exception("{}:{} Conflicting values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value))
  75. else:
  76. if show_warnings:
  77. warnings.warn("{}:{} Multiple values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value))
  78. else:
  79. cmake_settings[name] = DefineType(name, value, None, lineno)
  80. continue
  81. # look for "// pico_cmake_set_default BLAH_BLAH=42"
  82. m = re.match(r"^\s*//\s*pico_cmake_set_default\s+(\w+)\s*=\s*(.+?)\s*$", line)
  83. if m:
  84. #print(m.groups())
  85. name = m.group(1)
  86. value = m.group(2)
  87. # check all uppercase
  88. if name != name.upper():
  89. raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
  90. if name not in cmake_default_settings:
  91. cmake_default_settings[name] = DefineType(name, value, None, lineno)
  92. continue
  93. # look for "#else"
  94. m = re.match(r"^\s*#else\s*$", line)
  95. if m:
  96. validity_stack[-1] = not validity_stack[-1]
  97. continue
  98. # look for #endif
  99. m = re.match(r"^\s*#endif\s*$", line)
  100. if m:
  101. validity_stack.pop()
  102. continue
  103. if validity_stack[-1]:
  104. # look for "#include "foo.h"
  105. m = re.match(r"""^\s*#include\s+"(.+?)"\s*$""", line)
  106. if m:
  107. include = m.group(1)
  108. #print("Found nested include \"{}\" in {}".format(include, header_file))
  109. assert include.endswith(".h")
  110. # assume that the include is also in the boards directory
  111. assert "/" not in include or include.startswith("boards/")
  112. read_defines_from(os.path.join(os.path.dirname(board_header), os.path.basename(include)), defines)
  113. continue
  114. # look for "#if BLAH_BLAH"
  115. m = re.match(r"^\s*#if\s+(\w+)\s*$", line)
  116. if m:
  117. last_if = m.group(1)
  118. last_if_lineno = lineno
  119. validity_stack.append(bool(defines[last_if].resolved_value))
  120. continue
  121. # look for "#ifdef BLAH_BLAH"
  122. m = re.match(r"^\s*#ifdef\s+(\w+)\s*$", line)
  123. if m:
  124. last_ifdef = m.group(1)
  125. last_ifdef_lineno = lineno
  126. validity_stack.append(last_ifdef in defines)
  127. continue
  128. # look for "#ifndef BLAH_BLAH"
  129. m = re.match(r"^\s*#ifndef\s+(\w+)\s*$", line)
  130. if m:
  131. last_ifndef = m.group(1)
  132. last_ifndef_lineno = lineno
  133. validity_stack.append(last_ifndef not in defines)
  134. continue
  135. # look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42"
  136. m = re.match(r"^\s*#define\s+(\w+)(?:\s+(.+?))?\s*$", line)
  137. if m:
  138. #print(m.groups())
  139. name = m.group(1)
  140. value = m.group(2)
  141. # check all uppercase
  142. if name != name.upper():
  143. raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
  144. # check that adjacent #ifndef and #define lines match up
  145. if last_ifndef_lineno + 1 == lineno:
  146. if last_ifndef != name:
  147. raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name))
  148. if value:
  149. try:
  150. # most board-defines are integer values
  151. value = int(value, 0)
  152. except ValueError:
  153. pass
  154. # resolve nested defines
  155. resolved_value = value
  156. while resolved_value in defines_dict:
  157. resolved_value = defines_dict[resolved_value].resolved_value
  158. else:
  159. resolved_value = None
  160. # check for multiply-defined values
  161. if name in defines_dict:
  162. if defines_dict[name].value != value:
  163. raise Exception("{}:{} Conflicting definitions for {} ({} and {})".format(board_header, lineno, name, defines_dict[name].value, value))
  164. else:
  165. if show_warnings:
  166. warnings.warn("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines_dict[name].value, value))
  167. else:
  168. defines_dict[name] = DefineType(name, value, resolved_value, lineno)
  169. if board_header_basename == "amethyst_fpga.h":
  170. defines['PICO_RP2350'] = DefineType('PICO_RP2350', 1, 1, -1)
  171. with open(board_header) as header_fh:
  172. last_ifndef = None
  173. last_ifndef_lineno = -1
  174. validity_stack = [True]
  175. board_detection_is_next = False
  176. for lineno, line in enumerate(header_fh.readlines()):
  177. lineno += 1
  178. # strip trailing comments
  179. line = re.sub(r"(?<=\S)\s*//.*$", "", line)
  180. # look for board-detection comment
  181. if re.match("^\s*// For board detection", line):
  182. board_detection_is_next = True
  183. continue
  184. # check include-suggestion
  185. m = re.match("^\s*// This header may be included by other board headers as \"(.+?)\"", line)
  186. if m:
  187. include_suggestion = m.group(1)
  188. if include_suggestion == expected_include_suggestion:
  189. has_include_suggestion = True
  190. else:
  191. raise Exception("{}:{} Suggests including \"{}\" but file is named \"{}\"".format(board_header, lineno, include_suggestion, expected_include_suggestion))
  192. continue
  193. # look for "// pico_cmake_set BLAH_BLAH=42"
  194. m = re.match(r"^\s*//\s*pico_cmake_set\s+(\w+)\s*=\s*(.+?)\s*$", line)
  195. if m:
  196. #print(m.groups())
  197. name = m.group(1)
  198. value = m.group(2)
  199. # check all uppercase
  200. if name != name.upper():
  201. raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
  202. # check for multiply-defined values
  203. if name in cmake_settings:
  204. raise Exception("{}:{} Multiple values for pico_cmake_set {} ({} and {})".format(board_header, lineno, name, cmake_settings[name].value, value))
  205. else:
  206. if value:
  207. try:
  208. # most cmake settings are integer values
  209. value = int(value, 0)
  210. except ValueError:
  211. pass
  212. cmake_settings[name] = DefineType(name, value, None, lineno)
  213. continue
  214. # look for "// pico_cmake_set_default BLAH_BLAH=42"
  215. m = re.match(r"^\s*//\s*pico_cmake_set_default\s+(\w+)\s*=\s*(.+?)\s*$", line)
  216. if m:
  217. #print(m.groups())
  218. name = m.group(1)
  219. value = m.group(2)
  220. # check all uppercase
  221. if name != name.upper():
  222. raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
  223. # check for multiply-defined values
  224. if name in cmake_default_settings:
  225. raise Exception("{}:{} Multiple values for pico_cmake_set_default {} ({} and {})".format(board_header, lineno, name, cmake_default_settings[name].value, value))
  226. else:
  227. if value:
  228. try:
  229. # most cmake settings are integer values
  230. value = int(value, 0)
  231. except ValueError:
  232. pass
  233. cmake_default_settings[name] = DefineType(name, value, None, lineno)
  234. continue
  235. # look for "#else"
  236. m = re.match(r"^\s*#else\s*$", line)
  237. if m:
  238. validity_stack[-1] = not validity_stack[-1]
  239. continue
  240. # look for #endif
  241. m = re.match(r"^\s*#endif\s*$", line)
  242. if m:
  243. validity_stack.pop()
  244. continue
  245. if validity_stack[-1]:
  246. # look for "#include "foo.h"
  247. m = re.match(r"""^\s*#include\s+"(.+?)"\s*$""", line)
  248. if m:
  249. include = m.group(1)
  250. #print("Found include \"{}\" in {}".format(include, board_header))
  251. assert include.endswith(".h")
  252. # assume that the include is also in the boards directory
  253. assert "/" not in include or include.startswith("boards/")
  254. read_defines_from(os.path.join(os.path.dirname(board_header), os.path.basename(include)), defines)
  255. continue
  256. # look for "#if BLAH_BLAH"
  257. m = re.match(r"^\s*#if\s+(!)?\s*(\w+)\s*$", line)
  258. if m:
  259. valid = bool(defines[m.group(2)].resolved_value)
  260. if m.group(1):
  261. valid = not valid
  262. validity_stack.append(valid)
  263. continue
  264. # look for "#ifdef BLAH_BLAH"
  265. m = re.match(r"^\s*#ifdef\s+(\w+)\s*$", line)
  266. if m:
  267. validity_stack.append(m.group(1) in defines)
  268. continue
  269. # look for "#ifndef BLAH_BLAH"
  270. m = re.match(r"^\s*#ifndef\s+(\w+)\s*$", line)
  271. if m:
  272. last_ifndef = m.group(1)
  273. last_ifndef_lineno = lineno
  274. validity_stack.append(last_ifndef not in defines)
  275. continue
  276. # look for "#define BLAH_BLAH" or "#define BLAH_BLAH 42"
  277. m = re.match(r"^\s*#define\s+(\w+)(?:\s+(.+?))?\s*$", line)
  278. if m:
  279. #print(m.groups())
  280. name = m.group(1)
  281. value = m.group(2)
  282. # check all uppercase
  283. if name != name.upper():
  284. raise Exception("{}:{} Expected \"{}\" to be all uppercase".format(board_header, lineno, name))
  285. # check that adjacent #ifndef and #define lines match up
  286. if last_ifndef_lineno + 1 == lineno:
  287. if last_ifndef != name:
  288. raise Exception("{}:{} #ifndef {} / #define {} mismatch".format(board_header, last_ifndef_lineno, last_ifndef, name))
  289. if value:
  290. try:
  291. # most board-defines are integer values
  292. value = int(value, 0)
  293. except ValueError:
  294. pass
  295. # resolve nested defines
  296. resolved_value = value
  297. while resolved_value in defines:
  298. resolved_value = defines[resolved_value].resolved_value
  299. else:
  300. resolved_value = None
  301. # check the include-guard define
  302. if re.match(r"^_BOARDS_(\w+)_H$", name):
  303. # check it has an #ifndef
  304. if last_ifndef_lineno +1 != lineno:
  305. raise Exception("{}:{} Include-guard #define {} is missing an #ifndef".format(board_header, lineno, name))
  306. if value:
  307. raise Exception("{}:{} Include-guard #define {} shouldn't have a value".format(board_header, lineno, name))
  308. if len(defines) and not (len(defines) == 1 and defines[list(defines.keys())[0]].lineno < 0):
  309. raise Exception("{}:{} Include-guard #define {} should be the first define".format(board_header, lineno, name))
  310. if name == expected_include_guard:
  311. has_include_guard = True
  312. else:
  313. raise Exception("{}:{} Found include-guard #define {} but expected {}".format(board_header, lineno, name, expected_include_guard))
  314. # check board-detection define
  315. if board_detection_is_next:
  316. board_detection_is_next = False
  317. if value:
  318. raise Exception("{}:{} Board-detection #define {} shouldn't have a value".format(board_header, lineno, name))
  319. # this is a bit messy because pico.h does "#define RASPBERRYPI_PICO" and metrotech_xerxes_rp2040.h does "#define XERXES_RP2040"
  320. if name.endswith(expected_board_detection) or expected_board_detection.endswith(name):
  321. has_board_detection = True
  322. else:
  323. raise Exception("{}:{} Board-detection #define {} should end with {}".format(board_header, lineno, name, expected_board_detection))
  324. # check for multiply-defined values
  325. if name in defines:
  326. raise Exception("{}:{} Multiple definitions for {} ({} and {})".format(board_header, lineno, name, defines[name].value, value))
  327. else:
  328. defines[name] = DefineType(name, value, resolved_value, lineno)
  329. continue
  330. #import pprint; pprint.pprint(dict(sorted(defines.items(), key=lambda x: x[1].lineno)))
  331. #import pprint; pprint.pprint(dict(sorted(cmake_settings.items(), key=lambda x: x[1].lineno)))
  332. #import pprint; pprint.pprint(dict(sorted(cmake_default_settings.items(), key=lambda x: x[1].lineno)))
  333. chip = None
  334. if board_header_basename == "none.h":
  335. chip = 'RP2040'
  336. other_chip = 'RP2350'
  337. else:
  338. for setting in compulsory_cmake_settings:
  339. if setting not in cmake_settings:
  340. raise Exception("{} is missing a pico_cmake_set {} comment".format(board_header, setting))
  341. if cmake_settings['PICO_PLATFORM'].value == "rp2040":
  342. chip = 'RP2040'
  343. other_chip = 'RP2350'
  344. elif cmake_settings['PICO_PLATFORM'].value == "rp2350":
  345. other_chip = 'RP2040'
  346. if 'PICO_RP2350A' in defines and defines['PICO_RP2350A'].resolved_value == 1:
  347. chip = 'RP2350A'
  348. else:
  349. chip = 'RP2350B'
  350. if not board_header.endswith("amethyst_fpga.h"):
  351. if 'PICO_RP2350_A2_SUPPORTED' not in cmake_default_settings:
  352. raise Exception("{} uses chip {} but is missing a pico_cmake_set_default {} comment".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED'))
  353. if 'PICO_RP2350_A2_SUPPORTED' not in defines:
  354. raise Exception("{} uses chip {} but is missing a #define {}".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED'))
  355. if defines['PICO_RP2350_A2_SUPPORTED'].resolved_value != 1:
  356. raise Exception("{} sets #define {} {} (should be 1)".format(board_header, chip, 'PICO_RP2350_A2_SUPPORTED', defines['PICO_RP2350_A2_SUPPORTED'].resolved_value))
  357. for setting in compulsory_cmake_default_settings:
  358. if setting not in cmake_default_settings:
  359. raise Exception("{} is missing a pico_cmake_set_default {} comment".format(board_header, setting))
  360. for setting in matching_cmake_default_settings:
  361. if setting in cmake_default_settings and setting not in defines:
  362. raise Exception("{} has pico_cmake_set_default {} but is missing a matching #define".format(board_header, setting))
  363. elif setting in defines and setting not in cmake_default_settings:
  364. raise Exception("{} has #define {} but is missing a matching pico_cmake_set_default comment".format(board_header, setting))
  365. elif setting in defines and setting in cmake_default_settings:
  366. if cmake_default_settings[setting].value != defines[setting].resolved_value:
  367. raise Exception("{} has mismatched pico_cmake_set_default and #define values for {}".format(board_header, setting))
  368. for setting in compulsory_defines:
  369. if setting not in defines:
  370. raise Exception("{} is missing a #define {}".format(board_header, setting))
  371. if chip is None:
  372. raise Exception("Couldn't determine chip for {}".format(board_header))
  373. interfaces_json = chip_interfaces[chip]
  374. if not os.path.isfile(interfaces_json):
  375. raise Exception("{} doesn't exist".format(interfaces_json))
  376. with open(interfaces_json) as interfaces_fh:
  377. interface_pins = json.load(interfaces_fh)
  378. allowed_interfaces = interface_pins["interfaces"]
  379. allowed_pins = set(interface_pins["pins"])
  380. # convert instance-keys to integers (allowed by Python but not by JSON)
  381. for interface in allowed_interfaces:
  382. instances = allowed_interfaces[interface]["instances"]
  383. # can't modify a list that we're iterating over, so iterate over a copy
  384. instances_copy = list(instances)
  385. for instance in instances_copy:
  386. instance_num = int(instance)
  387. instances[instance_num] = instances.pop(instance)
  388. pins = dict() # dict of lists
  389. for name, define in defines.items():
  390. # check for other-chip defines
  391. if other_chip in name:
  392. raise Exception("{}:{} Header is for {} and so shouldn't have settings for {} ({})".format(board_header, define.lineno, chip, other_chip, name))
  393. # check for pin-conflicts
  394. if name.endswith("_PIN"):
  395. if define.resolved_value is None:
  396. raise Exception("{}:{} {} is set to an undefined value".format(board_header, define.lineno, name))
  397. elif not isinstance(define.resolved_value, int):
  398. raise Exception("{}:{} {} resolves to a non-integer value {}".format(board_header, define.lineno, name, define.resolved_value))
  399. else:
  400. if define.resolved_value in pins and define.resolved_value == define.value:
  401. if show_warnings:
  402. warnings.warn("{}:{} Both {} and {} claim to be pin {}".format(board_header, define.lineno, pins[define.resolved_value][0].name, name, define.resolved_value))
  403. pins[define.resolved_value].append(define)
  404. else:
  405. if define.resolved_value not in allowed_pins:
  406. raise Exception("{}:{} Pin {} for {} isn't a valid pin-number".format(board_header, define.lineno, define.resolved_value, name))
  407. pins[define.resolved_value] = [define]
  408. # check for invalid DEFAULT mappings
  409. m = re.match("^(PICO_DEFAULT_([A-Z0-9]+))_([A-Z0-9]+)_PIN$", name)
  410. if m:
  411. instance_name = m.group(1)
  412. interface = m.group(2)
  413. function = m.group(3)
  414. if interface == "WS2812":
  415. continue
  416. if interface not in allowed_interfaces:
  417. raise Exception("{}:{} {} is defined but {} isn't in {}".format(board_header, define.lineno, name, interface, interfaces_json))
  418. if instance_name not in defines:
  419. raise Exception("{}:{} {} is defined but {} isn't defined".format(board_header, define.lineno, name, instance_name))
  420. instance_define = defines[instance_name]
  421. instance_num = instance_define.resolved_value
  422. if instance_num not in allowed_interfaces[interface]["instances"]:
  423. raise Exception("{}:{} {} is set to an invalid instance {}".format(board_header, instance_define.lineno, instance_define, instance_num))
  424. interface_instance = allowed_interfaces[interface]["instances"][instance_num]
  425. if function not in interface_instance:
  426. raise Exception("{}:{} {} is defined but {} isn't a valid function for {}".format(board_header, define.lineno, name, function, instance_define))
  427. if define.resolved_value not in interface_instance[function]:
  428. raise Exception("{}:{} {} is set to {} which isn't a valid pin for {} on {} {}".format(board_header, define.lineno, name, define.resolved_value, function, interface, instance_num))
  429. # check that each used DEFAULT interface includes (at least) the expected pin-functions
  430. m = re.match("^PICO_DEFAULT_([A-Z0-9]+)$", name)
  431. if m:
  432. interface = m.group(1)
  433. if interface not in allowed_interfaces:
  434. raise Exception("{}:{} {} is defined but {} isn't in {}".format(board_header, define.lineno, name, interface, interfaces_json))
  435. if "expected_functions" in allowed_interfaces[interface]:
  436. expected_functions = allowed_interfaces[interface]["expected_functions"]
  437. if "required" in expected_functions:
  438. for function in expected_functions["required"]:
  439. expected_function_pin = "{}_{}_PIN".format(name, function)
  440. if expected_function_pin not in defines:
  441. raise Exception("{}:{} {} is defined but {} isn't defined".format(board_header, define.lineno, name, expected_function_pin))
  442. if "one_of" in expected_functions:
  443. expected_function_pins = list("{}_{}_PIN".format(name, function) for function in expected_functions["one_of"])
  444. if not any(func_pin in defines for func_pin in expected_function_pins):
  445. raise Exception("{}:{} {} is defined but none of {} are defined".format(board_header, define.lineno, name, list_to_string_with(expected_function_pins, "or")))
  446. if not has_include_guard:
  447. raise Exception("{} has no include-guard (expected {})".format(board_header, expected_include_guard))
  448. if not has_board_detection and expected_board_detection != "NONE":
  449. raise Exception("{} has no board-detection #define (expected {})".format(board_header, expected_board_detection))
  450. # lots of headers don't have this
  451. #if not has_include_suggestion:
  452. # raise Exception("{} has no include-suggestion (expected {})".format(board_header, expected_include_suggestion))
  453. # Check that #if / #ifdef / #ifndef / #else / #endif are correctly balanced
  454. assert len(validity_stack) == 1 and validity_stack[0]