header_reader.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. # Copyright 2022 DeepMind Technologies Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # ==============================================================================
  15. """Reads MuJoCo header files and generates a doc-friendly data structure."""
  16. import dataclasses
  17. import re
  18. from typing import Optional, List, Dict
  19. # Precompiled regex for matching a section.
  20. _SECTION_REGEX = re.compile(r'^//-+ (?P<section>.+) -+$')
  21. # Precompiled regex for matching a C function definition.
  22. _FUNCTION_REGEX = re.compile(r'(?P<token>mj\w+)\s*\(')
  23. # Precompiled regex for matching a C function ending.
  24. _FUNCTION_ENDING_REGEX = re.compile(r'\);\s$')
  25. # Precompiled regex for matching a C struct ending.
  26. _STRUCT_END_REGEX_1 = re.compile(r'^typedef\s+struct\s+\w+\s+(?P<token>mj\w+);')
  27. # Precompiled regex for matching a C struct ending (version 2).
  28. _STRUCT_END_REGEX_2 = re.compile(r'^}\s+(?P<token>mj\w+);')
  29. # Precompiled regex for matching a C enum ending.
  30. _ENUM_END_REGEX = re.compile(r'^}\s+(?P<token>mj\w+);')
  31. @dataclasses.dataclass
  32. class ApiDefinition:
  33. """Defines a C reference parsed from a C header file."""
  34. token: str
  35. c_type: str
  36. code: str
  37. start: int
  38. end: int
  39. section: str
  40. doc: str
  41. class ApiState:
  42. """Internal state of the reader used for parsing header files."""
  43. def __init__(self):
  44. self.token = ''
  45. self.section = ''
  46. self.code = ''
  47. self.doc = ''
  48. self._state = None
  49. self._start = 0
  50. self._end = 0
  51. @property
  52. def state(self):
  53. return self._state
  54. def export_definition(self):
  55. return ApiDefinition(self.token, self._state, self.code, self._start,
  56. self._end, self.section, self.doc)
  57. def start(self, state):
  58. self._state = state
  59. self._start = self._end
  60. def iterate(self):
  61. self._end += 1
  62. def end(self):
  63. self.token = ''
  64. self._state = None
  65. self.code = ''
  66. self.doc = ''
  67. def read(lines: List[str]) -> Dict[str, ApiDefinition]:
  68. """Reads header lines and returns a maps of ApiDefinition's."""
  69. api = {}
  70. stripped_functions = False
  71. s = ApiState()
  72. for line in lines:
  73. s.iterate()
  74. section = _find_section(line)
  75. if section is not None:
  76. if 'MJAPI FUNCTIONS' in section:
  77. # Stripped functions do not begin with MJAPI, and must be under the
  78. # predefiend section 'MJAPI FUNCTIONS'. This is because the docs don't
  79. # include this prefix, and so we need to read such functions from the
  80. # reference header.
  81. stripped_functions = True
  82. s.section = section
  83. s.end()
  84. continue
  85. if s.state == 'DOC':
  86. token = _find_function_start(line, stripped_functions)
  87. if token is not None:
  88. if stripped_functions:
  89. s.code = f'{s.code}{line}'
  90. else:
  91. s.code = f'{s.code}{line[6:]}'
  92. s.token = token
  93. s.start('FUNCTION')
  94. if _is_function_end(line):
  95. api[token] = s.export_definition()
  96. s.end()
  97. continue
  98. elif line.startswith('//'):
  99. s.doc = f'{s.doc}{line[3:]}'
  100. else:
  101. s.end()
  102. if s.state == 'FUNCTION':
  103. if stripped_functions:
  104. s.code = f'{s.code}{line}'
  105. else:
  106. s.code = f'{s.code}{line[6:]}'
  107. if _is_function_end(line):
  108. api[s.token] = s.export_definition()
  109. s.end()
  110. elif s.state == 'ENUM':
  111. match = _ENUM_END_REGEX.search(line)
  112. if match is not None:
  113. s.code = f'{s.code}{line}'
  114. s.token = match.group('token')
  115. api[s.token] = s.export_definition()
  116. s.end()
  117. else:
  118. s.code = f'{s.code}{line}'
  119. elif s.state == 'STRUCT':
  120. match = _STRUCT_END_REGEX_1.search(line)
  121. if match is None:
  122. match = _STRUCT_END_REGEX_2.search(line)
  123. if match is not None:
  124. s.code = f'{s.code}{line}'
  125. s.token = match.group('token')
  126. api[s.token] = s.export_definition()
  127. s.end()
  128. else:
  129. s.code = f'{s.code}{line}'
  130. elif s.state is None:
  131. if line.startswith('typedef enum'):
  132. s.start('ENUM')
  133. s.code = f'{s.code}{line}'
  134. if line.startswith('struct') or line.startswith('typedef struct'):
  135. s.start('STRUCT')
  136. s.code = f'{s.code}{line}'
  137. if line.startswith('//'):
  138. s.doc = f'{s.doc}{line[3:]}'
  139. s.start('DOC')
  140. token = _find_function_start(line, stripped_functions)
  141. if token is not None:
  142. if stripped_functions:
  143. s.code = f'{s.code}{line}'
  144. else:
  145. s.code = f'{s.code}{line[6:]}'
  146. s.token = token
  147. s.start('FUNCTION')
  148. if _is_function_end(line):
  149. api[token] = s.export_definition()
  150. s.end()
  151. return api
  152. def _find_section(line) -> Optional[str]:
  153. match = _SECTION_REGEX.search(line)
  154. if match is not None:
  155. return match.group('section').strip()
  156. return None
  157. def _find_function_start(line, stripped) -> Optional[str]:
  158. if (line.startswith('MJAPI') and 'extern' not in line) or stripped:
  159. match = _FUNCTION_REGEX.search(line)
  160. if match is not None:
  161. return match.group('token')
  162. return None
  163. def _is_function_end(line):
  164. match = _FUNCTION_ENDING_REGEX.search(line)
  165. return match is not None