NagaNithin-V
Deploy GraphForge OpenEnv — AST-parsed KG code-editing environment
7952f32
"""Per-template body codegen.
Each public ``render_<template>`` function takes the host node, its outgoing
edges in deterministic order, and returns a multi-line indented body suitable
for inserting after a ``def`` line. Bodies use only stdlib and never reference
unresolved names (the orchestrator ensures imports + pattern constants are
in scope).
Codegen is intentionally simple: the goal is *runnable, readable* Python that
respects template semantics, not optimal idiomatic code.
"""
from __future__ import annotations
from graphforge.graph.schema import Edge, Graph, Node
from graphforge.materializer import patterns
INDENT = " "
# ---- helpers ---------------------------------------------------------
def _kwargs_for(edge: Edge) -> str:
"""Render an edge's arg_mapping as ``param=arg, param2=arg2``."""
return ", ".join(f"{m.callee_param}={m.caller_arg}" for m in edge.arg_mapping)
def _callee_name(edge: Edge) -> str:
"""The local symbol used at the call site (just the function name).
The orchestrator emits ``from <module> import <name>`` for cross-module
callees, so the call site can always use the bare name.
"""
return edge.callee.split(".", 1)[1]
def _indent(lines: list[str]) -> str:
return "\n".join(INDENT + line for line in lines)
# ---- per-template renderers -----------------------------------------
def render_passthrough_call(node: Node, out_edges: list[Edge], _g: Graph) -> str:
if len(out_edges) != 1:
raise ValueError(
f"passthrough_call on {node.qualified_name} requires 1 out-edge, "
f"got {len(out_edges)}"
)
e = out_edges[0]
return _indent([f"return {_callee_name(e)}({_kwargs_for(e)})"])
def render_sequential_calls(node: Node, out_edges: list[Edge], _g: Graph) -> str:
if not out_edges:
raise ValueError(
f"sequential_calls on {node.qualified_name} requires >=1 out-edge"
)
lines: list[str] = []
for e in out_edges[:-1]:
lines.append(f"{_callee_name(e)}({_kwargs_for(e)})")
last = out_edges[-1]
lines.append(f"return {_callee_name(last)}({_kwargs_for(last)})")
return _indent(lines)
def render_validate_with_regex(node: Node, out_edges: list[Edge], _g: Graph) -> str:
if out_edges:
raise ValueError(
f"validate_with_regex on {node.qualified_name} must have 0 out-edges"
)
pattern_name = str(node.body_template_args.get("pattern", ""))
if patterns.get_pattern(pattern_name) is None:
raise ValueError(
f"unknown regex pattern {pattern_name!r} on {node.qualified_name}; "
f"known: {patterns.known_patterns()}"
)
constant = patterns.constant_name(pattern_name)
# The host signature is expected to be (s: str) -> bool — but we just use
# the first parameter name, whatever it is, to be tolerant.
from graphforge.actions.signature import parse_signature
parsed = parse_signature(node.signature)
if not parsed.parameters:
raise ValueError(
f"validate_with_regex on {node.qualified_name} requires "
f"at least one parameter"
)
arg = parsed.parameters[0].name
return _indent([f"return re.match({constant}, {arg}) is not None"])
def render_early_return_guard(node: Node, out_edges: list[Edge], _g: Graph) -> str:
if len(out_edges) != 1:
raise ValueError(
f"early_return_guard on {node.qualified_name} requires 1 out-edge"
)
condition = str(node.body_template_args.get("condition", "True"))
e = out_edges[0]
return _indent(
[
f"if not ({condition}):",
f"{INDENT}return None",
f"return {_callee_name(e)}({_kwargs_for(e)})",
]
)
def render_try_call_with_fallback(node: Node, out_edges: list[Edge], _g: Graph) -> str:
if len(out_edges) != 2:
raise ValueError(
f"try_call_with_fallback on {node.qualified_name} requires "
f"exactly 2 out-edges (primary, fallback)"
)
primary, fallback = out_edges
return _indent(
[
"try:",
f"{INDENT}return {_callee_name(primary)}({_kwargs_for(primary)})",
"except Exception:",
f"{INDENT}return {_callee_name(fallback)}({_kwargs_for(fallback)})",
]
)
def render_leaf_constant(node: Node, out_edges: list[Edge], _g: Graph) -> str:
if out_edges:
raise ValueError(
f"leaf_constant on {node.qualified_name} must have 0 out-edges"
)
if "value" not in node.body_template_args:
raise ValueError(
f"leaf_constant on {node.qualified_name} requires args.value"
)
value = node.body_template_args["value"]
return _indent([f"return {value!r}"])
# ---- registry --------------------------------------------------------
_RENDERERS: dict[str, object] = {
"passthrough_call": render_passthrough_call,
"sequential_calls": render_sequential_calls,
"validate_with_regex": render_validate_with_regex,
"early_return_guard": render_early_return_guard,
"try_call_with_fallback": render_try_call_with_fallback,
"leaf_constant": render_leaf_constant,
}
def render_body(node: Node, out_edges: list[Edge], graph: Graph) -> str:
"""Render the body for ``node`` based on its attached body template."""
if node.body_template is None:
# No body attached yet — emit a placeholder so the file still parses.
return _indent(['raise NotImplementedError("body not attached")'])
fn = _RENDERERS.get(node.body_template)
if fn is None:
raise ValueError(
f"no codegen for template {node.body_template!r} on {node.qualified_name}"
)
return fn(node, out_edges, graph) # type: ignore[operator]
def template_imports(template: str | None) -> set[str]:
"""Stdlib imports a template needs, beyond cross-module function imports."""
if template == "validate_with_regex":
return {"re"}
return set()