Skip to content

Commit db992cf

Browse files
mariusarvintedxoigmnCopilot
authored
Release 2026.1.1 (#472) (#31)
# 🎉 Major Updates - Added Python `ideas.cmake` module containing Python implementation of C build system instrumentation. - Re-introduced `ideas.ast_rust` module for Rust abstract syntax tree (AST) parsing in Python. - Added AST-based feedback to `ideas.wrapper` regarding deviations from the prescribed Rust template. # 🎈 Minor Updates - Upgraded `dspy` to 3.1.2. - Added automated functionality for extracting and renaming all `static` global variables in C code, to avoid possible name conflict UB in complex projects. - Correct usage of `dspy.Code` field annotations by creating persistent classes. - Upgraded secondary dependencies. - Attempting to build consolidated C code from `ideas.init` and error out if unsuccesful. --------- Co-authored-by: Cory Cornelius <cory.cornelius@intel.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5209422 commit db992cf

File tree

17 files changed

+681
-191
lines changed

17 files changed

+681
-191
lines changed

IDEAS.mk

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ endif
2121
RUSTFLAGS ?= -Awarnings## Ignore Rust compiler warnings
2222
CARGO_NET_OFFLINE ?= true## Cargo offline mode
2323
CFLAGS ?= -w## Ignore C compiler warnings
24+
export EXTRACT_INFO_CMAKE CFLAGS
2425

2526
GIT = git -C ${TRANSLATION_DIR}
2627

@@ -34,42 +35,17 @@ endif
3435

3536

3637
# cmake
37-
cmake: build-ninja/build.log
38-
39-
.PRECIOUS: build-ninja/CMakeCache.txt
40-
build-ninja/CMakeCache.txt: test_case/CMakeLists.txt ${EXTRACT_INFO_CMAKE}
41-
@rm -rf build-ninja
42-
ifeq ($(wildcard CMakePresets.json),)
43-
cmake -S test_case -B build-ninja -G Ninja \
44-
-DCMAKE_BUILD_TYPE=Debug \
45-
-DCMAKE_C_FLAGS_DEBUG="-g -O0" \
46-
-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${EXTRACT_INFO_CMAKE}" \
47-
-DCMAKE_C_FLAGS="${CFLAGS}" \
48-
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
49-
else
50-
cmake -S . --preset test \
51-
-DCMAKE_BUILD_TYPE=Debug \
52-
-DCMAKE_C_FLAGS_DEBUG="-g -O0" \
53-
-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES="${EXTRACT_INFO_CMAKE}" \
54-
-DCMAKE_C_FLAGS="${CFLAGS}" \
55-
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
56-
endif
57-
58-
.PRECIOUS: build-ninja/compile_commands.json
59-
build-ninja/compile_commands.json: build-ninja/CMakeCache.txt ;
38+
.PHONY: cmake
39+
cmake: build-ninja/cmake.log
6040

61-
.PRECIOUS: build-ninja/build.log
62-
build-ninja/build.log: build-ninja/CMakeCache.txt
63-
ifeq ($(wildcard CMakePresets.json),)
64-
-cmake --build build-ninja --target all 2> $@
65-
else
66-
-cmake --build build-ninja --target all --preset test 2> $@
67-
endif
68-
@find build-ninja -maxdepth 1 -type f -executable | \
69-
xargs -I{} sh -c "nm --extern-only {} | \
70-
awk '{if (\$$2 == \"T\") print \$$NF}' | \
71-
grep -v ^_ > {}.symbols"
41+
build-ninja/cmake.log: test_case/CMakeLists.txt ${EXTRACT_INFO_CMAKE}
42+
uv run python -m ideas.cmake source_dir=test_case \
43+
build_dir=build-ninja
44+
@touch $@
7245

46+
build-ninja/CMakeCache.txt: build-ninja/cmake.log
47+
build-ninja/compile_commands.json: build-ninja/cmake.log
48+
build-ninja/build.log: build-ninja/cmake.log
7349

7450
# init
7551
.PHONY: init
@@ -87,7 +63,7 @@ ${TRANSLATION_DIR}/.git/config:
8763
${GIT} commit --quiet --all --message "Initial commit"
8864

8965
.PRECIOUS: ${TRANSLATION_DIR}/Cargo.toml
90-
${TRANSLATION_DIR}/Cargo.toml: ${TRANSLATION_DIR}/.git/config
66+
${TRANSLATION_DIR}/Cargo.toml: | ${TRANSLATION_DIR}/.git/config
9167
echo -n "[workspace]\nresolver = \"3\"" > $@
9268
${GIT} add Cargo.toml
9369
${GIT} commit --quiet --all --message "Created cargo workspace"
@@ -180,16 +156,16 @@ ${TRANSLATION_DIR}/cargo_test.log: ${TRANSLATION_DIR}/build.log $(patsubst %,${T
180156
.PRECIOUS: ${TRANSLATION_DIR}/%/cargo_test.log
181157
${TRANSLATION_DIR}/%/cargo_test.log: ${TRANSLATION_DIR}/%/build.log ${TRANSLATION_DIR}/%/tests/test_cases.rs
182158
if [ $$(stat -c %s ${TRANSLATION_DIR}/$*/build.log) = 0 ]; then \
183-
cargo test --manifest-path ${TRANSLATION_DIR}/$*/Cargo.toml --test test_cases | tee $@ ; \
184-
else \
185-
find test_vectors -name '*.json' -exec echo "test {} ... FAILED" \; | tee $@ ; \
186-
fi \
159+
cargo test --manifest-path ${TRANSLATION_DIR}/$*/Cargo.toml --test test_cases | tee $@ ; \
160+
else \
161+
find test_vectors -name '*.json' -exec echo "test {} ... FAILED" \; | tee $@ ; \
162+
fi \
187163

188164
.PRECIOUS: ${TRANSLATION_DIR}/%/tests/test_cases.rs
189165
${TRANSLATION_DIR}/%/tests/test_cases.rs: | ${TEST_FILES} ${TRANSLATION_DIR}/%/Cargo.toml build-ninja/%.type
190166
@mkdir -p $(@D)
191-
cargo add --quiet --manifest-path ${TRANSLATION_DIR}/$*/Cargo.toml --dev assert_cmd@2.0.17 ntest@0.9.3 predicates@3.1.3
192-
-uv run python -m ideas.convert_tests ${TEST_FILES} --crate_manifest $(realpath ${TRANSLATION_DIR}/$*/Cargo.toml) | rustfmt > $@
167+
-uv run python -m ideas.convert_tests --crate_manifest $(realpath ${TRANSLATION_DIR}/$*/Cargo.toml) \
168+
${TEST_FILES} | rustfmt > $@
193169
${GIT} add $*/Cargo.toml $*/tests/test_cases.rs
194170
${GIT} commit --quiet --message "Converted \`$*\` test vectors"
195171

pyproject.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@ requires-python = "~=3.14.0"
88

99
dependencies = [
1010
"clang==21.1.7",
11-
"dspy==3.1.0",
11+
"dspy==3.1.2",
1212
"hydra-core",
13-
"tree-sitter==0.24.0",
14-
"tree-sitter-c==0.23.4",
15-
"tree-sitter-rust==0.23.2",
13+
"tree-sitter==0.25.2",
14+
"tree-sitter-rust==0.24.0",
1615
]
1716

1817
[dependency-groups]

src/ideas/adapters.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#
2+
# Copyright (C) 2026 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
7+
from unittest.mock import patch
8+
9+
from pydantic.fields import FieldInfo
10+
11+
import dspy
12+
import dspy.adapters.chat_adapter
13+
from dspy.adapters.chat_adapter import ChatAdapter as _ChatAdapter
14+
from dspy.adapters.utils import translate_field_type as _translate_field_type
15+
from dspy.signatures.utils import get_dspy_field_type
16+
17+
18+
class Code(dspy.Code):
19+
def format(self):
20+
return f"```{self.language.lower()}\n{self.code.rstrip()}\n```"
21+
22+
@classmethod
23+
def short_description(cls):
24+
return f"must be {cls.__name__}"
25+
26+
27+
class ChatAdapter(_ChatAdapter):
28+
def format_field_structure(self, signature: type[dspy.Signature]) -> str:
29+
with patch.object(
30+
dspy.adapters.chat_adapter, "translate_field_type", translate_field_type
31+
):
32+
return super().format_field_structure(signature)
33+
34+
35+
def translate_field_type(field_name: str, field_info: FieldInfo) -> str:
36+
# If a non-input field has a short_description, then use that.
37+
field_type = field_info.annotation
38+
if not field_type:
39+
raise RuntimeError(f"Field '{field_name}' is missing a type annotation")
40+
41+
if hasattr(field_type, "short_description") and get_dspy_field_type(field_info) != "input":
42+
desc = field_type.short_description()
43+
desc = (" " * 8) + f"# note: the value you produce {desc}" if desc else ""
44+
return f"{{{field_name}}}{desc}"
45+
return _translate_field_type(field_name, field_info)

src/ideas/ast_rust.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#
2+
# Copyright (C) 2026 Intel Corporation
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
#
6+
7+
from collections import OrderedDict
8+
9+
from tree_sitter import Language, Parser, Node, Query, QueryCursor
10+
import tree_sitter_rust
11+
12+
# Initialize the Rust language once
13+
RUST_LANGUAGE = Language(tree_sitter_rust.language())
14+
RUST_PARSER = Parser(RUST_LANGUAGE)
15+
16+
17+
class RustFnSignature:
18+
def __init__(self, node: Node):
19+
if not node.type == "function_item":
20+
raise ValueError(
21+
f"Node {node} is not a function_item, so cannot extract a signature!"
22+
)
23+
24+
name = node.child_by_field_name("name")
25+
if not name:
26+
raise ValueError(f"Function name not found in {node}!")
27+
28+
self.name: Node = name
29+
self.params: Node | None = node.child_by_field_name("parameters")
30+
self.return_type: Node | None = node.child_by_field_name("return_type")
31+
32+
def __repr__(self) -> str:
33+
text = ""
34+
if _text := self.name.text:
35+
text += _text.decode()
36+
37+
if self.params and (_text := self.params.text):
38+
text += _text.decode()
39+
40+
if self.return_type and (_text := self.return_type.text):
41+
text += _text.decode()
42+
return text
43+
44+
def __eq__(self, other: object) -> bool:
45+
if not isinstance(other, RustFnSignature):
46+
return NotImplemented
47+
48+
return self.__repr__() == other.__repr__()
49+
50+
51+
def get_root(code: str) -> Node:
52+
tree = RUST_PARSER.parse(code.encode())
53+
return tree.root_node
54+
55+
56+
def get_nodes(node: Node, node_type: str | None = None) -> list[Node]:
57+
nodes = []
58+
for child in node.children:
59+
if not node_type or child.type == node_type:
60+
nodes.append(child)
61+
return nodes
62+
63+
64+
def get_ancestor_nodes(node: Node, node_type: str | None = None) -> list[Node]:
65+
ancestors = []
66+
# Excluding self
67+
current = node.parent
68+
while current:
69+
if not node_type or current.type == node_type:
70+
ancestors.append(current)
71+
current = current.parent
72+
73+
# Remove root node from ancestors
74+
return ancestors[:-1]
75+
76+
77+
def get_macro_nodes(root: Node, placeholder: str) -> list[Node]:
78+
# Query for all nodes containing macro invocation
79+
source = f"""
80+
(macro_invocation
81+
macro: (identifier) @macro_name
82+
(#eq? @macro_name "{placeholder}")) @macro
83+
"""
84+
85+
query = Query(RUST_LANGUAGE, source)
86+
cursor = QueryCursor(query)
87+
captures = cursor.captures(root)
88+
89+
# Collect all unique ancestors by walking up from each macro invocation
90+
ancestors = set()
91+
for macro_node in captures.get("macro", []):
92+
ancestors.update(get_ancestor_nodes(macro_node))
93+
94+
return list(ancestors)
95+
96+
97+
def validate_changes(code: str, template: str) -> OrderedDict[str, str]:
98+
code_root = get_root(code)
99+
template_root = get_root(template)
100+
101+
nodes = get_nodes(code_root)
102+
template_nodes = get_nodes(template_root)
103+
allowed_change_nodes = get_macro_nodes(template_root, "unimplemented")
104+
105+
scope_feedback = OrderedDict()
106+
107+
# Check for top-level changes
108+
if len(nodes) != len(template_nodes):
109+
scope_feedback["top_level_changes"] = (
110+
"The generated code modifies parts outside the function body.\n"
111+
"You must **only** modify the `unimplemented!()` function body and leave everything else **unchanged**!"
112+
)
113+
114+
# Check for allowed changes
115+
for template_node, node in zip(template_nodes, nodes):
116+
if not template_node.text == node.text:
117+
if (
118+
template_node not in allowed_change_nodes
119+
or not template_node.type == "function_item"
120+
):
121+
scope_feedback["top_level_changes"] = (
122+
"The generated code modifies parts outside the function body.\n"
123+
"You must **only** modify the `unimplemented!()` function body and leave everything else **unchanged**!"
124+
)
125+
126+
if not node.type == "function_item" or (node.type != template_node.type):
127+
scope_feedback["signature_changes"] = (
128+
"You must preserve the function signature in the template intact and **not modify it**!"
129+
)
130+
else:
131+
# Compare signatures
132+
template_signature = RustFnSignature(template_node)
133+
try:
134+
signature = RustFnSignature(node)
135+
except ValueError:
136+
signature = None
137+
138+
if template_signature != signature:
139+
scope_feedback["signature_changes"] = (
140+
"You must preserve the function signature in the template intact and **not modify it**!"
141+
)
142+
143+
return scope_feedback

0 commit comments

Comments
 (0)