Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions python/src/hhat_lang/dialects/heather/grammar/comment_grammar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Heather comment syntax helpers."""

from __future__ import annotations

import re


def strip_comments(code: str) -> str:
"""Remove comments using Heather comment syntax before parsing signatures."""

code = re.sub(r"/\-.*?\-/", "", code, flags=re.DOTALL)
return re.sub(r"//.*", "", code)
168 changes: 168 additions & 0 deletions python/src/hhat_lang/dialects/heather/grammar/doc_signatures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""Heather signature parsing helpers."""

from __future__ import annotations

import re
from pathlib import Path

from hhat_lang.dialects.heather.grammar.comment_grammar import strip_comments
from hhat_lang.toolchain.project.doc_signatures import CodeSignature, SignatureRow

_IDENTIFIER = r"@?[a-zA-Z][a-zA-Z0-9\-_]*"
_TYPE_VALUE = r"\[?@?[a-zA-Z][a-zA-Z0-9\-_.]*(?:<[^\s{}()]+>)?\]?"
_FUNCTION_RE = re.compile(
rf"(?<![\w-])fn\s+(?P<name>{_IDENTIFIER})\s*"
rf"\((?P<args>[^)]*)\)\s*(?P<type>{_TYPE_VALUE})?\s*\{{",
re.MULTILINE,
)
_TYPE_RE = re.compile(rf"(?<![\w-])type\s+(?P<name>{_IDENTIFIER})", re.MULTILINE)
_ARG_RE = re.compile(rf"(?P<name>{_IDENTIFIER})\s*:\s*(?P<type>{_TYPE_VALUE})")
_FIELD_RE = _ARG_RE


def paradigm(*values: str | None) -> str:
if any(value and value.strip().startswith("@") for value in values):
return "quantum"
return "classical"


def _parse_rows(raw_rows: str) -> tuple[SignatureRow, ...]:
return tuple(
SignatureRow(
name=match.group("name").strip(),
type=match.group("type").strip(),
paradigm=paradigm(match.group("name"), match.group("type")),
)
for match in _ARG_RE.finditer(raw_rows)
)


def _parse_function_signatures(code: str) -> tuple[CodeSignature, ...]:
signatures: list[CodeSignature] = []
for match in _FUNCTION_RE.finditer(code):
name = match.group("name").strip()
function_type = (match.group("type") or "").strip()
signatures.append(
CodeSignature(
name=name,
kind="function",
type=function_type,
paradigm=paradigm(name),
arguments=_parse_rows(match.group("args")),
)
)
return tuple(signatures)


def _find_matching_brace(code: str, open_brace_index: int) -> int | None:
depth = 0
for index in range(open_brace_index, len(code)):
if code[index] == "{":
depth += 1
elif code[index] == "}":
depth -= 1
if depth == 0:
return index
return None


def _parse_enum_variants(block: str) -> tuple[SignatureRow, ...]:
variants: list[SignatureRow] = []
index = 0
while index < len(block):
match = re.search(_IDENTIFIER, block[index:])
if match is None:
break

name = match.group(0)
name_start = index + match.start()
index = index + match.end()
next_index = index
while next_index < len(block) and block[next_index].isspace():
next_index += 1

if next_index < len(block) and block[next_index] == ":":
index = next_index + 1
continue

if next_index < len(block) and block[next_index] == "{":
variants.append(SignatureRow(name=name, type="Tagged", paradigm=paradigm(name)))
close_index = _find_matching_brace(block, next_index)
index = len(block) if close_index is None else close_index + 1
continue

if name_start > 0 and block[name_start - 1] in {":", "."}:
continue

variants.append(SignatureRow(name=name, type="Named", paradigm=paradigm(name)))

return tuple(variants)


def _parse_type_signatures(code: str) -> tuple[CodeSignature, ...]:
signatures: list[CodeSignature] = []
for match in _TYPE_RE.finditer(code):
name = match.group("name").strip()
index = match.end()
while index < len(code) and code[index].isspace():
index += 1

if index < len(code) and code[index] == ":":
line_end = code.find("\n", index)
if line_end == -1:
line_end = len(code)
type_value = code[index + 1 : line_end].strip()
signatures.append(
CodeSignature(
name=name,
kind="primitive",
type=type_value,
paradigm=paradigm(name),
)
)
continue

if index >= len(code) or code[index] != "{":
continue

close_index = _find_matching_brace(code, index)
if close_index is None:
continue

block = code[index + 1 : close_index]
fields = tuple(
SignatureRow(
name=field.group("name").strip(),
type=field.group("type").strip(),
paradigm=paradigm(field.group("name"), field.group("type")),
)
for field in _FIELD_RE.finditer(block)
)
block_without_fields = _FIELD_RE.sub(" ", block).strip()
has_tagged_variants = bool(re.search(rf"{_IDENTIFIER}\s*\{{", block))

if fields and not block_without_fields and not has_tagged_variants:
signatures.append(
CodeSignature(
name=name,
kind="struct",
paradigm=paradigm(name),
members=fields,
)
)
else:
signatures.append(
CodeSignature(
name=name,
kind="enum",
paradigm=paradigm(name),
variants=_parse_enum_variants(block),
)
)

return tuple(signatures)


def parse_code_signatures(code_file: Path) -> tuple[CodeSignature, ...]:
code = strip_comments(code_file.read_text(encoding="utf-8"))
return _parse_function_signatures(code) + _parse_type_signatures(code)
133 changes: 132 additions & 1 deletion python/src/hhat_lang/toolchain/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
from rich.panel import Panel

from hhat_lang.toolchain.project.new import (
create_new_const_file,
create_new_fn_file,
create_new_project,
create_new_type_file,
create_new_const_file,
)
from hhat_lang.toolchain.project.run import run_project
from hhat_lang.toolchain.project.update import update_project
from hhat_lang.toolchain.project.utils import get_proj_dir

app = typer.Typer(
Expand All @@ -28,6 +29,15 @@
console = Console()


def _get_update_dir(path: Path | None = None) -> Path:
current = path or Path().absolute()
while current != current.parent:
if (current / "src").is_dir():
return current
current = current.parent
return path or Path().absolute()


def version_callback(value: bool) -> None:
if value:
print("[bold royal_blue1]H-hat Language Toolchain[/] version [bold royal_blue1]0.1.0[/]")
Expand Down Expand Up @@ -70,6 +80,7 @@ def help(command: Optional[str] = typer.Argument(None, help="Command to get help
"[bold]Available commands:[/bold]\n"
" [bold]new[/bold] Create a new project, file, or type file\n"
" [bold]run[/bold] Run the current H-hat project\n"
" [bold]update[/bold] Check docs files and signatures\n"
" [bold]help[/bold] Show this help message\n\n"
"Use [bold]hat help <command>[/bold] for detailed information about a command.",
title="hat - Command Line Interface",
Expand Down Expand Up @@ -226,6 +237,126 @@ def new(
raise typer.Exit(1)


@app.command()
def update(
dialect: str = typer.Option(
"heather",
"--dialect",
"-d",
help="Dialect parser to use for code signatures.",
),
) -> None:
"""
Update the current H-hat project metadata and documentation files.

This command can be executed from a H-hat project directory or one of its
subdirectories. It checks whether every .hat code file under src/ has a matching .md
documentation file under docs/, creating missing documentation files while
preserving the source directory layout.

Example:
hat update # Create missing docs for src/*.hat files
hat update --dialect foo # Use hhat_lang.dialects.foo grammar parser
"""
try:
proj_dir = _get_update_dir()
result = update_project(proj_dir, dialect=dialect)

messages: list[str] = []
if result.created_docs:
created_files = "\n".join(
f" {doc_file.relative_to(proj_dir)}" for doc_file in result.created_docs
)
messages.append(
f"Created {result.created_count} documentation file(s):\n{created_files}"
)
else:
messages.append("All code files already have documentation counterparts.")

if result.orphaned_docs:
orphaned_files = "\n".join(
f" {doc.original_path.relative_to(proj_dir)} -> "
f"{doc.orphan_path.relative_to(proj_dir)}"
for doc in result.orphaned_docs
)
messages.append(
f"Renamed {result.orphaned_doc_count} orphan documentation file(s):\n"
f"{orphaned_files}"
)

if result.removed_signatures:
removed_signature_lines = "\n".join(
f" {signature.doc_file.relative_to(proj_dir)} :: {signature.name}"
for signature in result.removed_signatures
)
messages.append(
f"Removed {result.removed_signature_count} stale documentation signature(s):\n"
f"{removed_signature_lines}"
)

if result.updated_signatures:
updated_lines = "\n".join(
f" {update.doc_file.relative_to(proj_dir)} :: "
f"{update.signature.name} ({update.reason})"
for update in result.updated_signatures
)
messages.append(
f"Updated {result.updated_signature_count} documentation signature(s):\n"
f"{updated_lines}"
)

if result.signature_mismatches:
mismatch_lines = "\n".join(
f" {mismatch.doc_file.relative_to(proj_dir)} :: "
f"{mismatch.signature.name} ({mismatch.reason})"
for mismatch in result.signature_mismatches
)
messages.append(
f"Unable to update {result.signature_mismatch_count} signature mismatch(es) "
f"out of {result.checked_signature_count} checked signature(s):\n"
f"{mismatch_lines}"
)
title = "⚠ Documentation signature mismatch"
border_style = "yellow"
else:
messages.append(
f"All {result.checked_signature_count} code signature(s) match documentation."
)
title = "✓ Documentation check complete"
border_style = "green"

console.print(
Panel(
"\n\n".join(messages),
title=title,
border_style=border_style,
)
)
if result.signature_mismatches:
raise typer.Exit(1)
except typer.Exit:
raise
except ValueError as e:
console.print(
Panel(
str(e) + "\n\nPlease make sure you're inside a H-hat project directory.",
title="⚠ Error",
border_style="red",
)
)
raise typer.Exit(1)
except Exception as e:
console.print(
Panel(
f"An error occurred while updating the project: {str(e)}\n\n"
"Please check your project structure and try again.",
title="⚠ Error",
border_style="red",
)
)
raise typer.Exit(1)


@app.command()
def run() -> None:
"""
Expand Down
Loading