Files
Mujoco_WASM/doc/generate/generate_mjcf_table.py
T
Yuval Tassa 2544c34997 Clean up new schema code.
- Declare the full child lists of the body-alias elements.
- Verify read-table coverage: every generated row array must be consumed.
- Fix stale attribute facts on hand-read elements.

PiperOrigin-RevId: 958685667
Change-Id: I4a914f3136a5078eb8ca24aa4e162d55afafd923
2026-08-03 18:15:39 -07:00

187 lines
6.6 KiB
Python

# Copyright 2026 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Generates the MJCF[] grammar table from src/xml/mjcf.schema.
The table (src/xml/generated/mjcf_table.inc) is the element tree consumed by the
mjXSchema validator: rows of {name, cardinality, attributes...} with
{"<"}/{">"} nesting markers. It is checked in and gated by
test/doc/doc_test.py, which regenerates it from the schema and diffs.
"""
import sys
import os
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _SCRIPT_DIR)
import mjcf_schema
_REPO_ROOT = os.path.dirname(os.path.dirname(_SCRIPT_DIR))
SCHEMA_PATH = os.path.join(_REPO_ROOT, 'src', 'xml', 'mjcf.schema')
_HEADER = '''\
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// GENERATED FILE, DO NOT EDIT. Generated from src/xml/mjcf.schema by
// doc/generate/generate_mjcf_table.py; test/doc/doc_test.py checks freshness.
//
// The MJCF grammar table consumed by mjXSchema: rows of {name, cardinality,
// attributes...} with {"<"}/{">"} nesting markers. worldbody, frame and
// replicate have no rows: mjXSchema::NameMatch validates them against the
// body row (see the alias= facets in mjcf.schema).
// clang-format off
'''
_WIDTH = 100
def _wrap_row(parts: list[str], indent: int) -> list[str]:
"""One {...}, initializer, wrapped at _WIDTH with hanging indent."""
lines = []
line = ' ' * indent + '{' + parts[0]
for part in parts[1:]:
candidate = f'{line}, {part}'
if len(candidate) + 2 > _WIDTH: # room for "},"
lines.append(line + ',')
line = ' ' * (indent + 4) + part
else:
line = candidate
lines.append(line + '},')
return lines
KIND_CHAR = {'exclusive': 'e', 'together': 't', 'requires': 'r',
'oneof': 'o'}
def _element_constraints(schema, element):
"""Element's own constraints plus those of transitively used groups."""
cons = list(element.constraints())
visited = set()
stack = [m.group for m in element.members
if isinstance(m, mjcf_schema.Use)]
while stack:
name = stack.pop()
if name in visited:
continue
visited.add(name)
group = schema.groups[name]
for member in group.members:
if isinstance(member, mjcf_schema.Constraint):
cons.append(member)
elif isinstance(member, mjcf_schema.Use):
stack.append(member.group)
return cons
def generate() -> str:
"""Generates the C++ grammar table header file contents from mjcf.schema."""
schema = mjcf_schema.parse_file(SCHEMA_PATH)
out = []
constraints = []
count = 0
def emit_entry(lines: list[str]):
nonlocal count
out.extend(lines)
count += 1
def visit(element: mjcf_schema.Element, card: str, indent: int,
project: bool):
# a row in default context is the element's defaultable projection
attrs = [a for a in schema.expanded_attrs(element)]
if project:
attrs = [a for a in attrs
if a.name not in ('name', 'class')
and not a.facets.get('nodefault')]
parts = [f'"{element.xml_name()}"', f'"{card}"']
parts += [f'"{a.name}"' for a in attrs]
row_index = count
emit_entry(_wrap_row(parts, indent))
# presence constraints whose attributes all survive in this row
row_attrs = {a.name for a in attrs}
for con in _element_constraints(schema, element):
if all(all(n in row_attrs for n in b) for b in con.bundles):
spec = '|'.join(' '.join(b) for b in con.bundles)
constraints.append(f" {{{row_index}, '{KIND_CHAR[con.kind]}', "
f'"{spec}"}},')
# self-recursion is implied by card R; alias elements (worldbody, frame,
# replicate) have no rows -- mjXSchema::NameMatch admits their tags
# against the body row
children = [c for c in element.children()
if c.name != element.name
and 'alias' not in schema.elements[c.name].facets]
if project:
# plugin configuration is not settable per-class
children = [c for c in children if c.name != 'plugin']
if not children:
return
emit_entry([' ' * indent + '{"<"},'])
for child in children:
decl = schema.elements[child.name]
child_project = project or (
element.name == 'default' and not child.name.startswith('default_'))
visit(decl, child.card, indent + 4, child_project)
if indent == 0:
out.append('')
emit_entry([' ' * indent + '{">"},'])
visit(schema.elements['mujoco'], '!', 0, False)
body = '\n'.join(out)
con_body = '\n'.join(constraints)
return (_HEADER +
'std::vector<const char*> MJCF[] = {\n' + body + '\n};\n'
'// clang-format on\n\n'
'const int nMJCF = sizeof(MJCF) / sizeof(MJCF[0]);\n\n'
'// presence constraints, indexed into MJCF[]; enforced by\n'
'// mjXSchema::Check. spec: attribute bundles, space-joined,\n'
"// '|'-separated; kind: e=exclusive t=together r=requires o=oneof\n"
'// clang-format off\n'
'const mjXConstraintDef MJCF_constraints[] = {\n' + con_body +
'\n};\n'
'// clang-format on\n\n'
'const int nMJCF_constraints = '
'sizeof(MJCF_constraints) / sizeof(MJCF_constraints[0]);\n')
def main() -> int:
if len(sys.argv) > 2:
sys.exit('usage: generate_mjcf_table.py [output.inc]')
text = generate()
if len(sys.argv) == 2:
with open(sys.argv[1], 'w', encoding='utf-8') as file:
file.write(text)
else:
sys.stdout.write(text)
return 0
if __name__ == '__main__':
sys.exit(main())