#!/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' {module_filename} - Z3 Go API\n') f.write(' \n') f.write('\n\n') f.write('
\n') f.write(f'

{module_filename}

\n') f.write(f'

{description}

\n') f.write('
\n') f.write('
\n') f.write(' \n') # Types section if types_info: f.write('

Types

\n') for type_name in sorted(types_info.keys()): type_data = types_info[type_name] f.write('
\n') f.write(f'

type {type_name}

\n') if type_data['comment']: f.write(f'

{type_data["comment"]}

\n') # Methods if type_data['methods']: f.write('
\n') f.write('

Methods:

\n') for method in sorted(type_data['methods'], key=lambda m: m['name']): f.write('
\n') f.write(f'

{method["name"]}

\n') f.write(f'
{method["signature"]}
\n') if method['comment']: f.write(f'

{method["comment"]}

\n') f.write('
\n') f.write('
\n') f.write('
\n') # Package functions section if functions_info: f.write('

Functions

\n') f.write('
\n') for func in sorted(functions_info, key=lambda f: f['name']): f.write('
\n') f.write(f'

{func["name"]}

\n') f.write(f'
{func["signature"]}
\n') if func['comment']: f.write(f'

{func["comment"]}

\n') f.write('
\n') f.write('
\n') if not types_info and not functions_info: f.write('

No public API documentation extracted. See godoc for complete reference.

\n') f.write(' \n') f.write('
\n') f.write('\n\n') print(f"Generated module page: {output_path}") def generate_html_docs(go_api_path, output_dir): """Generate HTML documentation for Go bindings.""" # Create output directory os.makedirs(output_dir, exist_ok=True) # Go source files and their descriptions go_files = { 'z3.go': 'Core types (Context, Config, Symbol, Sort, Expr, FuncDecl, Quantifier, Lambda) and basic operations', 'solver.go': 'Solver and Model API for SMT solving', 'tactic.go': 'Tactics, Goals, Probes, and Parameters for goal-based solving', 'arith.go': 'Arithmetic operations (integers, reals) and comparisons', 'array.go': 'Array operations (select, store, constant arrays)', 'bitvec.go': 'Bit-vector operations and constraints', 'fp.go': 'IEEE 754 floating-point operations', 'seq.go': 'Sequences, strings, and regular expressions', 'datatype.go': 'Algebraic datatypes, tuples, and enumerations', 'optimize.go': 'Optimization with maximize/minimize objectives', 'fixedpoint.go': 'Fixedpoint solver for Datalog and constrained Horn clauses (CHC)', 'log.go': 'Interaction logging for debugging and analysis', } # Generate main index.html index_path = os.path.join(output_dir, 'index.html') with open(index_path, 'w', encoding='utf-8') as f: f.write('\n') f.write('\n') f.write('\n') f.write(' \n') f.write(' \n') f.write(' Z3 Go API Documentation\n') f.write(' \n') f.write('\n') f.write('\n') f.write('
\n') f.write('

Z3 Go API Documentation

\n') f.write('

Go bindings for the Z3 Theorem Prover

\n') f.write('
\n') f.write('
\n') # Overview section f.write('
\n') f.write('

Overview

\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

\n') f.write('
\n') # Quick start f.write('
\n') f.write('

Quick Start

\n') f.write('
\n') f.write('
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('
\n') f.write('
\n') # Installation f.write('
\n') f.write('

Installation

\n') f.write('
\n') f.write('

Prerequisites:

\n') f.write('
    \n') f.write('
  • Go 1.20 or later
  • \n') f.write('
  • Z3 built as a shared library
  • \n') f.write('
  • CGO enabled (default)
  • \n') f.write('
\n') f.write('

Build Z3 with Go bindings:

\n') f.write('
\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('
\n') f.write('

Set environment variables:

\n') f.write('
\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('
\n') f.write('
\n') f.write('
\n') # API modules with detailed documentation f.write('
\n') f.write('

API Modules

\n') for filename, description in go_files.items(): file_path = os.path.join(go_api_path, filename) if os.path.exists(file_path): module_name = filename.replace('.go', '') # Generate individual module page generate_module_page(filename, description, go_api_path, output_dir) # Extract types and functions from the file types, functions = extract_types_and_functions(file_path) f.write(f'
\n') f.write(f'

{filename}

\n') f.write(f'

{description}

\n') if types: f.write('

Types: ') f.write(', '.join([f'{t}' for t in sorted(types)])) f.write('

\n') if functions: # Filter public functions public_funcs = [f for f in functions if f and len(f) > 0 and f[0].isupper()] if public_funcs: 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('

\n') f.write(f'

→ View full API reference

\n') f.write('
\n') f.write('
\n') # Features section f.write('
\n') f.write('

Features

\n') f.write(' \n') f.write('
\n') # Resources f.write('
\n') f.write('

Resources

\n') f.write(' \n') f.write('
\n') f.write(' ← Back to main API documentation\n') f.write('
\n') f.write('\n') f.write('\n') print(f"Generated Go documentation index at: {index_path}") return True def main(): args = parse_args() print("Z3 Go API Documentation Generator") print("=" * 50) # Check if Go is installed go_cmd = check_go_installed() # Verify Go API path exists if not os.path.exists(args.go_api_path): print(f"ERROR: Go API path does not exist: {args.go_api_path}") return 1 # Generate documentation print(f"\nGenerating documentation from: {args.go_api_path}") print(f"Output directory: {args.output_dir}") # Try godoc first if Go is available godoc_success = False if go_cmd: godoc_success = generate_godoc_markdown(go_cmd, args.go_api_path, args.output_dir) # Always generate our custom HTML documentation if not generate_html_docs(args.go_api_path, args.output_dir): print("ERROR: Failed to generate documentation") return 1 if godoc_success: print("\n✓ Generated both godoc markdown and custom HTML documentation") print("\n" + "=" * 50) print("Documentation generated successfully!") print(f"Open {os.path.join(args.output_dir, 'index.html')} in your browser.") return 0 if __name__ == '__main__': try: sys.exit(main()) except KeyboardInterrupt: print("\nInterrupted by user") sys.exit(1) except Exception as e: print(f"ERROR: {e}") import traceback traceback.print_exc() sys.exit(1)