#!/usr/bin/env python # Copyright (c) Microsoft Corporation 2025 """ Z3 Go API documentation generator script This script generates HTML documentation for the Z3 Go bindings. It creates a browsable HTML interface for the Go API documentation. """ import os import sys import subprocess import argparse import re SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) GO_API_PATH = os.path.join(SCRIPT_DIR, '..', 'src', 'api', 'go') OUTPUT_DIR = os.path.join(SCRIPT_DIR, 'api', 'html', 'go') def extract_types_and_functions(filepath): """Extract type and function names from a Go source file.""" types = [] functions = [] try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() # Extract type declarations type_pattern = r'type\s+(\w+)\s+(?:struct|interface)' types = re.findall(type_pattern, content) # Extract function/method declarations # Match both: func Name() and func (r *Type) Name() func_pattern = r'func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(' functions = re.findall(func_pattern, content) except Exception as e: print(f"Warning: Could not parse {filepath}: {e}") return types, functions def extract_detailed_api(filepath): """Extract detailed type and function information with signatures and comments.""" types_info = {} functions_info = [] context_methods = [] # Special handling for Context methods try: with open(filepath, 'r', encoding='utf-8') as f: lines = f.readlines() i = 0 while i < len(lines): line = lines[i].strip() # Extract type with comment if line.startswith('type ') and ('struct' in line or 'interface' in line): # Look back for comment comment = "" j = i - 1 while j >= 0 and (lines[j].strip().startswith('//') or lines[j].strip() == ''): if lines[j].strip().startswith('//'): comment = lines[j].strip()[2:].strip() + " " + comment j -= 1 match = re.match(r'type\s+(\w+)\s+', line) if match: type_name = match.group(1) types_info[type_name] = { 'comment': comment.strip(), 'methods': [] } # Extract function/method with signature and comment if line.startswith('func '): # Look back for comment comment = "" j = i - 1 while j >= 0 and (lines[j].strip().startswith('//') or lines[j].strip() == ''): if lines[j].strip().startswith('//'): comment = lines[j].strip()[2:].strip() + " " + comment j -= 1 # Extract full signature (may span multiple lines) signature = line k = i + 1 while k < len(lines) and '{' not in signature: signature += ' ' + lines[k].strip() k += 1 # Remove body if '{' in signature: signature = signature[:signature.index('{')].strip() # Parse receiver if method method_match = re.match(r'func\s+\(([^)]+)\)\s+(\w+)', signature) func_match = re.match(r'func\s+(\w+)', signature) if method_match: receiver = method_match.group(1).strip() func_name = method_match.group(2) # Extract receiver type receiver_type = receiver.split()[-1].strip('*') # Only add if function name is public if func_name[0].isupper(): if receiver_type == 'Context': # Special handling for Context methods - add to context_methods context_methods.append({ 'name': func_name, 'signature': signature, 'comment': comment.strip() }) elif receiver_type in types_info: types_info[receiver_type]['methods'].append({ 'name': func_name, 'signature': signature, 'comment': comment.strip() }) elif func_match: func_name = func_match.group(1) # Only add if it's public (starts with capital) if func_name[0].isupper(): functions_info.append({ 'name': func_name, 'signature': signature, 'comment': comment.strip() }) i += 1 # If we have Context methods but no other content, return them as functions if context_methods and not types_info and not functions_info: functions_info = context_methods elif context_methods: # Add Context pseudo-type types_info['Context'] = { 'comment': 'Context methods (receiver omitted for clarity)', 'methods': context_methods } except Exception as e: print(f"Warning: Could not parse detailed API from {filepath}: {e}") return types_info, functions_info def extract_package_comment(filepath): """Extract the package-level documentation comment from a Go file.""" try: with open(filepath, 'r', encoding='utf-8') as f: lines = f.readlines() comment_lines = [] in_comment = False for line in lines: stripped = line.strip() if stripped.startswith('/*'): in_comment = True comment_lines.append(stripped[2:].strip()) elif in_comment: if '*/' in stripped: comment_lines.append(stripped.replace('*/', '').strip()) break comment_lines.append(stripped.lstrip('*').strip()) elif stripped.startswith('//'): comment_lines.append(stripped[2:].strip()) elif stripped.startswith('package'): break return ' '.join(comment_lines).strip() if comment_lines else None except Exception as e: return None def parse_args(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-o', '--output-dir', dest='output_dir', default=OUTPUT_DIR, help='Output directory for documentation (default: %(default)s)', ) parser.add_argument('--go-api-path', dest='go_api_path', default=GO_API_PATH, help='Path to Go API source files (default: %(default)s)', ) return parser.parse_args() def check_go_installed(): """Check if Go is installed and available.""" try: # Try to find go in common locations go_paths = [ 'go', 'C:\\Program Files\\Go\\bin\\go.exe', 'C:\\Go\\bin\\go.exe', ] for go_cmd in go_paths: try: result = subprocess.run([go_cmd, 'version'], capture_output=True, text=True, check=True, timeout=5) print(f"Found Go: {result.stdout.strip()}") return go_cmd except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): continue print("WARNING: Go is not installed or not in PATH") print("Install Go from https://golang.org/dl/ for enhanced documentation") return None except Exception as e: print(f"WARNING: Could not check Go installation: {e}") return None def extract_package_comment(go_file_path): """Extract package-level documentation comment from a Go file.""" try: with open(go_file_path, 'r', encoding='utf-8') as f: lines = f.readlines() in_comment = False comment_lines = [] for line in lines: stripped = line.strip() if stripped.startswith('/*'): in_comment = True comment_lines.append(stripped[2:].strip()) elif in_comment: if stripped.endswith('*/'): comment_lines.append(stripped[:-2].strip()) break comment_lines.append(stripped.lstrip('*').strip()) elif stripped.startswith('//'): comment_lines.append(stripped[2:].strip()) elif stripped.startswith('package '): break return ' '.join(comment_lines).strip() except Exception as e: print(f"Warning: Could not extract comment from {go_file_path}: {e}") return "" def generate_godoc_markdown(go_cmd, go_api_path, output_dir): """Generate markdown documentation using godoc.""" print("Generating documentation with godoc...") os.makedirs(output_dir, exist_ok=True) try: # Change to the Go API directory orig_dir = os.getcwd() os.chdir(go_api_path) # Run go doc to get package documentation result = subprocess.run( [go_cmd, 'doc', '-all'], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: # Create markdown file doc_text = result.stdout godoc_md = os.path.join(output_dir, 'godoc.md') with open(godoc_md, 'w', encoding='utf-8') as f: f.write('# Z3 Go API Documentation (godoc)\n\n') f.write(doc_text) print(f"Generated godoc markdown at: {godoc_md}") os.chdir(orig_dir) return True else: print(f"godoc returned error: {result.stderr}") os.chdir(orig_dir) return False except Exception as e: print(f"Error generating godoc markdown: {e}") try: os.chdir(orig_dir) except: pass return False def generate_module_page(module_filename, description, go_api_path, output_dir): """Generate a detailed HTML page for a single Go module.""" file_path = os.path.join(go_api_path, module_filename) if not os.path.exists(file_path): return module_name = module_filename.replace('.go', '') output_path = os.path.join(output_dir, f'{module_name}.html') # Extract detailed API information types_info, functions_info = extract_detailed_api(file_path) with open(output_path, 'w', encoding='utf-8') as f: f.write('\n\n
\n') f.write(' \n') f.write(f'{description}
\n') f.write('{type_data["comment"]}
\n') # Methods if type_data['methods']: f.write('{method["comment"]}
\n') f.write('{func["comment"]}
\n') f.write('No public API documentation extracted. See godoc for complete reference.
\n') f.write(' \n') f.write('Go bindings for the Z3 Theorem Prover
\n') f.write('The Z3 Go bindings provide idiomatic Go access to the Z3 SMT solver. These bindings use CGO to wrap the Z3 C API and provide automatic memory management through Go finalizers.
\n') f.write('Package: github.com/Z3Prover/z3/src/api/go
package main\n\n')
f.write('import (\n')
f.write(' "fmt"\n')
f.write(' "github.com/Z3Prover/z3/src/api/go"\n')
f.write(')\n\n')
f.write('func main() {\n')
f.write(' // Create a context\n')
f.write(' ctx := z3.NewContext()\n\n')
f.write(' // Create integer variable\n')
f.write(' x := ctx.MkIntConst("x")\n\n')
f.write(' // Create solver\n')
f.write(' solver := ctx.NewSolver()\n\n')
f.write(' // Add constraint: x > 0\n')
f.write(' zero := ctx.MkInt(0, ctx.MkIntSort())\n')
f.write(' solver.Assert(ctx.MkGt(x, zero))\n\n')
f.write(' // Check satisfiability\n')
f.write(' if solver.Check() == z3.Satisfiable {\n')
f.write(' fmt.Println("sat")\n')
f.write(' model := solver.Model()\n')
f.write(' if val, ok := model.Eval(x, true); ok {\n')
f.write(' fmt.Println("x =", val.String())\n')
f.write(' }\n')
f.write(' }\n')
f.write('}\n')
f.write(' Prerequisites:
\n') f.write('Build Z3 with Go bindings:
\n') f.write('# Using CMake\n')
f.write('mkdir build && cd build\n')
f.write('cmake -DZ3_BUILD_GO_BINDINGS=ON -DZ3_BUILD_LIBZ3_SHARED=ON ..\n')
f.write('make\n\n')
f.write('# Using Python build script\n')
f.write('python scripts/mk_make.py --go\n')
f.write('cd build && make\n')
f.write(' Set environment variables:
\n') f.write('export CGO_CFLAGS="-I${Z3_ROOT}/src/api"\n')
f.write('export CGO_LDFLAGS="-L${Z3_ROOT}/build -lz3"\n')
f.write('export LD_LIBRARY_PATH="${Z3_ROOT}/build:$LD_LIBRARY_PATH"\n')
f.write(' {description}
\n') if types: f.write('Types: ')
f.write(', '.join([f'{t}' for t in sorted(types)]))
f.write('
Key Functions: ')
# Show first 15 functions to keep it manageable
funcs_to_show = sorted(public_funcs)[:15]
f.write(', '.join([f'{func}()' for func in funcs_to_show]))
if len(public_funcs) > 15:
f.write(f' (+{len(public_funcs)-15} more)')
f.write('