diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3e385c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,69 @@ + +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Install base dependencies and Python 3 +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + git \ + wget \ + curl \ + python3 \ + python3-dev \ + python3-pip \ + g++ \ + flex \ + bison \ + ca-certificates \ + unzip \ + libgc-dev \ + libpcre3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Provide compatibility symlinks for Shedskin's expected libgctba +RUN if [ -f /usr/lib/x86_64-linux-gnu/libgccpp.so ]; then \ + ln -sf /usr/lib/x86_64-linux-gnu/libgccpp.so /usr/lib/x86_64-linux-gnu/libgctba.so; \ + fi && \ + if [ -f /usr/lib/x86_64-linux-gnu/libgccpp.a ]; then \ + ln -sf /usr/lib/x86_64-linux-gnu/libgccpp.a /usr/lib/x86_64-linux-gnu/libgctba.a; \ + fi + +# Install Shedskin (latest version via pip3) +# Shedskin now works with Python 3 (versions 0.9.8+) +RUN python3 -m pip install --upgrade pip setuptools wheel && \ + SHEDSKIN_LATEST=$(curl -s https://api.github.com/repos/shedskin/shedskin/releases/latest | grep '"tag_name"' | cut -d'"' -f4) && \ + echo "Installing Shedskin version: $SHEDSKIN_LATEST" && \ + python3 -m pip install "shedskin@git+https://github.com/shedskin/shedskin.git@${SHEDSKIN_LATEST}#egg=shedskin" || \ + (echo "Warning: Shedskin git installation failed. Attempting pip fallback..." && \ + python3 -m pip install shedskin || \ + echo "Warning: Shedskin installation failed completely." && true) + +WORKDIR /workspace +VOLUME ["/workspace"] + +# Install additional dependencies for ESBMC +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libboost-all-dev \ + libz3-dev \ + libc6 \ + libstdc++6 \ + && rm -rf /var/lib/apt/lists/* + +# Download and install latest ESBMC release (esbmc-linux.zip) +# Always fetches the latest release from GitHub +RUN ESBMC_DOWNLOAD_URL=$(curl -s https://api.github.com/repos/esbmc/esbmc/releases/latest | \ + grep -o 'https://github.com/esbmc/esbmc/releases/download/[^"]*esbmc-linux.zip' | head -n 1) && \ + echo "Downloading ESBMC from: $ESBMC_DOWNLOAD_URL" && \ + curl -L "$ESBMC_DOWNLOAD_URL" -o esbmc-linux.zip && \ + unzip -q esbmc-linux.zip && \ + mv bin/* /usr/local/bin/ && \ + mv lib/* /usr/local/lib/ && \ + mv include/* /usr/local/include/ 2>/dev/null || true && \ + rm -rf esbmc-linux.zip bin lib include license README release-notes.txt && \ + ldconfig && \ + which esbmc && esbmc --version + +ENTRYPOINT ["/bin/bash"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9e549ee --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +DOCKER_IMAGE ?= esbmc-shedskin + +.PHONY: docker-build docker-shell docker-run-example + +docker-build: + docker build -t $(DOCKER_IMAGE) . + +docker-shell: docker-build + docker run --rm -it -v "$(PWD)":/workspace -w /workspace $(DOCKER_IMAGE) + +# Smoke: convert, compile, and run the Shedskin runtime example inside Docker +docker-run-example: docker-build + docker run --rm -it -v "$(PWD)":/workspace -w /workspace $(DOCKER_IMAGE) \ + -lc "set -e; rm -rf /tmp/shedskin-smoke; mkdir -p /tmp/shedskin-smoke; cp -r /workspace/examples /tmp/shedskin-smoke/; cd /tmp/shedskin-smoke/examples; exec bash" diff --git a/README.md b/README.md index 5b2f1e1..a8f548b 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ **ESBMC-Python-CPP** is a work-in-progress toolkit that bridges Python code and C/++ verification through the [ESBMC](https://esbmc.org/) model checker. It supports three primary approaches for converting and analyzing Python code: -1. **Static Conversion via Shedskin** +1. **Static Conversion via Shedskin** Converts Python to C++ code using [Shedskin](https://github.com/shedskin/shedskin). + - See [`docs/shedskin.md`](docs/shedskin.md) for a reproducible Docker setup and usage examples. + - See [`docs/hybrid-flow.md`](docs/hybrid-flow.md) for a detailed explanation of the hybrid verification flow. -2. **LLM-Based Conversion from Python to C** +2. **LLM-Based Conversion from Python to C** Uses local or remote LLMs to directly convert Python code to verifiable C code. -3. **Dynamic Tracing** +3. **Dynamic Tracing** Traces a specific execution path to analyze behavior during runtime. --- @@ -241,8 +243,30 @@ export OPENAI_API_BASE=http://localhost:8080/v1 --- +## 🐳 Using Docker with Shedskin + +For a fully reproducible environment with Shedskin and ESBMC, you can use the provided Dockerfile: + +```bash +# 1. Rebuild the image (force refresh of dependencies) +docker build --no-cache -t esbmc-shedskin . +# or +make docker-build + +# 2. Open an interactive shell with examples copied to /tmp/shedskin-smoke/examples +make docker-run-example + +# 3. Inside the container (already in /tmp/shedskin-smoke/examples): +shedskin shedskin_runtime_smoke.py +make +./shedskin_runtime_smoke +``` + +You can replace `shedskin_runtime_smoke.py` with any other Python example that is compatible with Shedskin. + +--- + ## 📬 Contact & Contributions This project is under active development. Feedback, issues, and contributions are welcome! - diff --git a/dict.hpp b/dict.hpp index 36210e4..6ae2c05 100644 --- a/dict.hpp +++ b/dict.hpp @@ -158,7 +158,63 @@ class dict { __ss_int __len__() const { return size_; } + + // Get value or default + V get(const K& key, const V& default_val) const { + for (__ss_int i = 0; i < SIZE; i++) { + if (used[i] && keys[i] == key) { + return values[i]; + } + } + return default_val; + } + + // Return list of keys + list* keys_list() const { + list* out = new list(); + for (__ss_int i = 0; i < SIZE; i++) { + if (used[i]) { + out->append(keys[i]); + } + } + return out; + } + + // Return list of values + list* values_list() const { + list* out = new list(); + for (__ss_int i = 0; i < SIZE; i++) { + if (used[i]) { + out->append(values[i]); + } + } + return out; + } + + // Return list of (key, value) tuples + list*>* items_list() const { + list*>* out = new list*>(); + for (__ss_int i = 0; i < SIZE; i++) { + if (used[i]) { + tuple2* t = new tuple2(2, keys[i], values[i]); + out->append(t); + } + } + return out; + } + + list* keys() const { + return keys_list(); + } + + list* values() const { + return values_list(); + } + + list*>* items() const { + return items_list(); + } }; } // namespace shedskin -#endif \ No newline at end of file +#endif diff --git a/examples/fprime_threading_smoke.py b/examples/fprime_threading_smoke.py new file mode 100644 index 0000000..55ee2cb --- /dev/null +++ b/examples/fprime_threading_smoke.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""F' threading-inspired smoke test for Shedskin (no method passing).""" + +from typing import Any, Dict, List, Optional + + +class TaskContext: + """Deterministic task runner mimicking an F' rate group.""" + + def __init__(self, name: str): + self.name = name + self._tasks: List[Any] = [] + self._queue: List[Any] = [] + + def add_task(self, task: Any): + self._tasks.append(task) + + def schedule_async(self, task: Any): + self._queue.append(task) + + def run_once(self): + for task in self._tasks: + task.run() + while self._queue: + job = self._queue.pop(0) + job.run() + + +class AsyncJob: + def __init__(self, handler: Any, data: Any): + self.handler = handler + self.data = data + + def run(self): + self.handler.handle(self.data) + + +class InputPort: + """Simplified F' input port com contexto opcional.""" + + def __init__(self, name: str, data_type: str): + self.name = name + self.data_type = data_type + self._handler: Optional[Any] = None + self._context: Optional[TaskContext] = None + + def register_handler(self, handler: Any, context: Optional[TaskContext] = None): + self._handler = handler + self._context = context + + def invoke(self, data: Any): + if not self._handler: + return + if self._context: + job = AsyncJob(self._handler, data) + self._context.schedule_async(job) + else: + self._handler.handle(data) + + +class OutputPort: + """Simplified F' output port that fans out to multiple inputs.""" + + def __init__(self, name: str, data_type: str): + self.name = name + self.data_type = data_type + self._connections: List[InputPort] = [] + + def connect(self, input_port: InputPort): + if input_port.data_type == self.data_type: + self._connections.append(input_port) + + def emit(self, data: Any): + for connection in self._connections: + connection.invoke(data) + + +class CommandMessage: + def __init__(self, command_id: int, args: Dict[str, int]): + self.command_id = command_id + self.args = args + + +class CommandComponent: + """Produces command messages for downstream components.""" + + def __init__(self): + self.command_out = OutputPort("command_out", "CommandMessage") + + def issue(self, command_id: int, payload: Dict[str, int]): + msg = CommandMessage(command_id, payload) + self.command_out.emit(msg) + return msg + + +class TickTask: + def __init__(self, component: "ExecutionComponent"): + self.component = component + + def run(self): + self.component.event_log.append("tick") + + +class CommandHandler: + def __init__(self, component: "ExecutionComponent"): + self.component = component + + def handle(self, message: CommandMessage): + keys = sorted(list(message.args.keys())) + parts: List[str] = [] + for key in keys: + value = message.args[key] + parts.append(f"{key}={value}") + description = f"cmd:{message.command_id}|" + ",".join(parts) + self.component.event_log.append(description) + + +class ExecutionComponent: + """Consumes commands and logs execution order.""" + + def __init__(self): + self.command_in = InputPort("command_in", "CommandMessage") + self.context = TaskContext("exec") + self.event_log: List[str] = [] + self.tick_task = TickTask(self) + self.handler = CommandHandler(self) + self.context.add_task(self.tick_task) + + def register(self): + self.command_in.register_handler(self.handler, self.context) + + +def run_smoke() -> List[str]: + commander = CommandComponent() + executor = ExecutionComponent() + executor.register() + commander.command_out.connect(executor.command_in) + + # Set operations exercise the runtime helpers + targets = set(["nav", "nav", "science"]) + assert len(targets) == 2 + targets.discard("nav") + assert "nav" not in targets + + commander.issue(1, {"speed": 3, "heading": 90}) + executor.context.run_once() + + commander.issue(2, {"speed": 1}) + commander.issue(3, {"speed": 2}) + executor.context.run_once() + + expected = [ + "tick", + "cmd:1|heading=90,speed=3", + "tick", + "cmd:2|speed=1", + "cmd:3|speed=2", + ] + assert executor.event_log == expected + return executor.event_log + + +if __name__ == "__main__": + log = run_smoke() + print("F' threading smoke log:", log) diff --git a/examples/shedskin_example_simple.py b/examples/shedskin_example_simple.py new file mode 100644 index 0000000..6d87c78 --- /dev/null +++ b/examples/shedskin_example_simple.py @@ -0,0 +1,10 @@ +def inc(x): + return x + 1 + +def main(): + a = 0 + a = inc(a) + assert a == 1 + +if __name__ == '__main__': + main() diff --git a/examples/shedskin_runtime_smoke.py b/examples/shedskin_runtime_smoke.py new file mode 100644 index 0000000..80fb63e --- /dev/null +++ b/examples/shedskin_runtime_smoke.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +# Simple runtime smoke test for Shedskin-generated runtime helpers + +def test_list_ops(): + xs = [1, 2, 2, 3] + assert xs.count(2) == 2 + assert xs.index(2) == 1 + xs.remove(2) + assert xs == [1, 2, 3] + try: + xs.remove(99) + raise AssertionError("remove should have failed") + except Exception: + pass + + +def test_dict_ops(): + d = {"a": 1, "b": 2} + assert d.get("a", 0) == 1 + assert d.get("missing", 42) == 42 + k = sorted(list(d.keys())) + v = sorted(list(d.values())) + items = sorted(list(d.items())) + assert k == ["a", "b"] + assert v == [1, 2] + assert items == [("a", 1), ("b", 2)] + + +def test_str_ops(): + s = "hello world" + assert s.startswith("he") + assert s.endswith("world") + assert s.find("lo") == 3 + assert s.find("zz") == -1 + assert s.rfind("l") == 9 + + +def test_set_ops(): + st = set([1, 2, 3]) + st.add(3) + assert 2 in st + st.discard(2) + assert 2 not in st + popped = st.pop() + assert popped in [1, 3] + try: + while True: + st.pop() + except Exception: + pass + + +def main(): + test_list_ops() + test_dict_ops() + test_str_ops() + test_set_ops() + print("shedskin runtime smoke: OK") + + +if __name__ == "__main__": + main() diff --git a/fprime_complex_ros_prepared.py b/fprime_complex_ros_prepared.py new file mode 100644 index 0000000..915989e --- /dev/null +++ b/fprime_complex_ros_prepared.py @@ -0,0 +1,121 @@ +from typing import Dict, Any +import stubs.ros_mock as rospy +from stubs.ros_mock import Twist + + +class EmptyQueue(Exception): + pass + + +class SimpleQueue: + def __init__(self): + self._items = [] + + +class SimulationState: + + def __init__(self): + self.position = {'x': 0.0, 'y': 0.0, 'z': 0.0} + self.orientation = {'roll': 0.0, 'pitch': 0.0, 'yaw': 0.0} + self.sensor_data = {} + +class ROSComponent: + """Base class for ROS components""" + + def __init__(self, name: str): + self.name = name + self.publishers: Dict[str, rospy.Publisher] = {} + self.message_queue = SimpleQueue() + + def create_publisher(self, topic: str, msg_type, queue_size: int=10): + self.publishers[topic] = rospy.Publisher(topic, msg_type, queue_size=queue_size) + +class FPrimeComponent: + """Base class for F Prime components""" + + def __init__(self, name: str): + self.name = name + +class ROSFPrimeBridge: + """Minimal bridge stub for Shedskin.""" + + def __init__(self): + self.running = False + + def start(self): + self.running = True + + def stop(self): + self.running = False + +class RoverSimulation: + """Main simulation class integrating ROS and F Prime components""" + + def __init__(self): + rospy.init_node('rover_simulation') + self.state = SimulationState() + self.bridge = ROSFPrimeBridge() + self.ros_components: Dict[str, ROSComponent] = {} + self.fprime_components: Dict[str, FPrimeComponent] = {} + + def add_ros_component(self, component: ROSComponent): + self.ros_components[component.name] = component + + def add_fprime_component(self, component: FPrimeComponent): + self.fprime_components[component.name] = component + + def start(self): + """Start the simulation""" + self.bridge.start() + rospy.loginfo('Simulation started') + + def stop(self): + """Stop the simulation""" + self.bridge.stop() + rospy.loginfo('Simulation stopped') + + def update_state(self, new_state: dict): + """Update simulation state""" + if 'position' in new_state: + self.state.position = new_state['position'] + if 'orientation' in new_state: + self.state.orientation = new_state['orientation'] + if 'sensor_data' in new_state: + self.state.sensor_data = new_state['sensor_data'] + +class MotorController(ROSComponent): + """Example ROS component for motor control""" + + def __init__(self): + ROSComponent.__init__(self, 'motor_controller') + self.create_publisher('/cmd_vel', Twist) + + def set_velocity(self, linear: float, angular: float): + msg = Twist() + msg.linear.x = linear + msg.angular.z = angular + self.publishers['/cmd_vel'].publish(msg) + +class NavigationComponent(FPrimeComponent): + """Example F Prime component for navigation""" + + def __init__(self): + FPrimeComponent.__init__(self, 'navigation') + +def main(): + sim = RoverSimulation() + motor_controller = MotorController() + nav_component = NavigationComponent() + sim.add_ros_component(motor_controller) + sim.add_fprime_component(nav_component) + + sim.start() + steps = 3 + i = 0 + while i < steps: + sim.update_state({'position': {'x': 1.0, 'y': 2.0, 'z': 0.0}}) + motor_controller.set_velocity(0.5, 0.1) + i = i + 1 + sim.stop() +if __name__ == '__main__': + main() diff --git a/list.hpp b/list.hpp index 63d56e0..0e3d754 100644 --- a/list.hpp +++ b/list.hpp @@ -458,6 +458,78 @@ class list : public pyobj { return value; // Returns the deleted element } + // Count occurrences of a value + __ss_int count(const T& value) const { + __ss_int cnt = 0; + Node* current = head; + while (current) { + if (current->data == value) { + cnt++; + } + current = current->next; + } + return cnt; + } + + // Find first index of value (supports negative start), raises if not found + __ss_int index(const T& value, __ss_int start = 0) const { + if (start < 0) { + start = size_ + start; + } + if (start < 0) start = 0; + if (start >= size_) { + throw std::out_of_range("list.index(x): x not in list"); + } + + Node* current = head; + for (__ss_int i = 0; i < start && current; i++) { + current = current->next; + } + + __ss_int idx = start; + while (current) { + if (current->data == value) { + return idx; + } + current = current->next; + idx++; + } + throw std::out_of_range("list.index(x): x not in list"); + } + + // Remove first occurrence; raises if not found + void remove(const T& value) { + if (!head) { + throw std::out_of_range("list.remove(x): x not in list"); + } + + Node* current = head; + Node* previous = nullptr; + __ss_int idx = 0; + while (current) { + if (current->data == value) { + if (previous) { + previous->next = current->next; + if (current == tail) { + tail = previous; + } + } else { + head = current->next; + if (!head) { + tail = nullptr; + } + } + delete current; + size_--; + return; + } + previous = current; + current = current->next; + idx++; + } + throw std::out_of_range("list.remove(x): x not in list"); + } + }; @@ -532,4 +604,4 @@ list* sorted(list* lst, __ss_int start, __ss_int key, __ss_int reverse) { } // namespace shedskin -#endif \ No newline at end of file +#endif diff --git a/scripts/shedskin_prepare.py b/scripts/shedskin_prepare.py new file mode 100644 index 0000000..3245736 --- /dev/null +++ b/scripts/shedskin_prepare.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +"""Utility to massage Python files into a Shedskin-friendly subset.""" + +import argparse +import ast +import copy + + +def is_dataclass_decorator(dec): + if isinstance(dec, ast.Name): + return dec.id == "dataclass" + if isinstance(dec, ast.Attribute): + return dec.attr == "dataclass" + return False + + +def is_enum_base(base): + if isinstance(base, ast.Name): + return base.id == "Enum" + if isinstance(base, ast.Attribute): + return base.attr == "Enum" + return False + + +class DataclassTransformer(ast.NodeTransformer): + def visit_ClassDef(self, node): # noqa: N802 + node = self.generic_visit(node) + has_dataclass = any(is_dataclass_decorator(d) for d in node.decorator_list) + node.decorator_list = [d for d in node.decorator_list if not is_dataclass_decorator(d)] + + has_enum = any(is_enum_base(base) for base in node.bases) + if has_enum: + node.bases = [] + + if has_dataclass: + if not any(isinstance(stmt, ast.FunctionDef) and stmt.name == "__init__" for stmt in node.body): + node.body.insert(0, self._build_init(node)) + return node + + def _build_init(self, node): + fields = [] + for stmt in node.body: + if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): + fields.append((stmt.target.id, stmt.value)) + + args = [ast.arg(arg="self")] + defaults = [] + for name, default in fields: + args.append(ast.arg(arg=name)) + defaults.append(default if default is not None else ast.Constant(value=None)) + + init_body = [] + for name, _ in fields: + assign = ast.Assign( + targets=[ast.Attribute(value=ast.Name(id="self", ctx=ast.Load()), attr=name, ctx=ast.Store())], + value=ast.Name(id=name, ctx=ast.Load()), + ) + init_body.append(assign) + if not init_body: + init_body.append(ast.Pass()) + + init_func = ast.FunctionDef( + name="__init__", + args=ast.arguments( + posonlyargs=[], + args=args, + vararg=None, + kwonlyargs=[], + kw_defaults=[], + kwarg=None, + defaults=defaults, + ), + body=init_body, + decorator_list=[], + returns=None, + ) + return init_func + + +class LambdaLifter(ast.NodeTransformer): + def __init__(self): + self.counter = 0 + self.new_funcs = [] + + def visit_Module(self, node): # noqa: N802 + node = self.generic_visit(node) + node.body.extend(self.new_funcs) + return node + + def visit_Lambda(self, node): # noqa: N802 + name = f"__shedskin_lambda_{self.counter}" + self.counter += 1 + func_def = ast.FunctionDef( + name=name, + args=copy.deepcopy(node.args), + body=[ast.Return(value=self.visit(node.body))], + decorator_list=[], + returns=None, + ) + ast.fix_missing_locations(func_def) + self.new_funcs.append(func_def) + return ast.Name(id=name, ctx=ast.Load()) + + +class AttrCallRewriter(ast.NodeTransformer): + def __init__(self): + self.need_has = False + self.need_set = False + + def visit_Module(self, node): # noqa: N802 + node = self.generic_visit(node) + helpers = [] + if self.need_has: + helpers.append(self._make_has_helper()) + if self.need_set: + helpers.append(self._make_set_helper()) + node.body = helpers + node.body + return node + + def visit_Call(self, node): # noqa: N802 + node = self.generic_visit(node) + if isinstance(node.func, ast.Name): + if node.func.id == "hasattr" and len(node.args) == 2: + self.need_has = True + node.func = ast.Name(id="__shedskin_hasattr", ctx=ast.Load()) + elif node.func.id == "setattr" and len(node.args) == 3: + self.need_set = True + node.func = ast.Name(id="__shedskin_setattr", ctx=ast.Load()) + return node + + def _make_has_helper(self): + obj = ast.Name(id="obj", ctx=ast.Load()) + attr = ast.Name(id="name", ctx=ast.Load()) + body = [ + ast.Return( + value=ast.Compare( + left=attr, + ops=[ast.In()], + comparators=[ast.Attribute(value=obj, attr="__dict__", ctx=ast.Load())], + ) + ) + ] + helper = ast.FunctionDef( + name="__shedskin_hasattr", + args=ast.arguments( + posonlyargs=[], + args=[ast.arg(arg="obj"), ast.arg(arg="name")], + vararg=None, + kwonlyargs=[], + kw_defaults=[], + kwarg=None, + defaults=[], + ), + body=body, + decorator_list=[], + returns=None, + ) + return helper + + def _make_set_helper(self): + obj = ast.Name(id="obj", ctx=ast.Load()) + attr = ast.Name(id="name", ctx=ast.Load()) + value = ast.Name(id="value", ctx=ast.Load()) + assign = ast.Assign( + targets=[ast.Subscript(value=ast.Attribute(value=obj, attr="__dict__", ctx=ast.Load()), slice=attr, ctx=ast.Store())], + value=value, + ) + helper = ast.FunctionDef( + name="__shedskin_setattr", + args=ast.arguments( + posonlyargs=[], + args=[ast.arg(arg="obj"), ast.arg(arg="name"), ast.arg(arg="value")], + vararg=None, + kwonlyargs=[], + kw_defaults=[], + kwarg=None, + defaults=[], + ), + body=[assign, ast.Return(value=ast.Constant(value=None))], + decorator_list=[], + returns=None, + ) + return helper + + +def transform_source(source: str) -> str: + tree = ast.parse(source) + tree = AttrCallRewriter().visit(tree) + tree = LambdaLifter().visit(tree) + tree = DataclassTransformer().visit(tree) + ast.fix_missing_locations(tree) + return ast.unparse(tree) + + +def main(): + parser = argparse.ArgumentParser(description="Prepare Python file for Shedskin") + parser.add_argument("input", help="Input Python file") + parser.add_argument("output", help="Output Python file") + args = parser.parse_args() + + with open(args.input, "r", encoding="utf-8") as f: + src = f.read() + + transformed = transform_source(src) + + with open(args.output, "w", encoding="utf-8") as f: + f.write(transformed) + + +if __name__ == "__main__": + main() diff --git a/set.hpp b/set.hpp index 4dadc12..30ca7c4 100644 --- a/set.hpp +++ b/set.hpp @@ -34,6 +34,28 @@ class set : public pyobj { return false; } + bool __contains__(const T& value) const { + return contains(value); + } + + void discard(const T& value) { + for (__ss_int i = 0; i < len(items); i++) { + if (items->__getfast__(i) == value) { + items->__remove__(i); + return; + } + } + } + + T pop() { + if (len(items) == 0) { + throw std::out_of_range("pop from an empty set"); + } + T value = items->__getfast__(0); + items->__remove__(0); + return value; + } + __ss_int __len__() const { return len(items); } @@ -43,6 +65,22 @@ class set : public pyobj { return items->__getfast__(index); } + class for_in_loop { + typename list::Iterator it; + typename list::Iterator end_it; + public: + for_in_loop() : it(nullptr), end_it(nullptr) {} + for_in_loop(set& s) : it(s.items->begin()), end_it(s.items->end()) {} + bool __next__(T& ref) { + if (it != end_it) { + ref = *it; + ++it; + return true; + } + return false; + } + }; + ~set() { delete items; } diff --git a/str.hpp b/str.hpp index 3b18cb9..944887d 100644 --- a/str.hpp +++ b/str.hpp @@ -57,6 +57,50 @@ class str : public pyobj { operator const char*() const { return data; } + + // Check prefix + bool startswith(const str& prefix, __ss_int start = 0) const { + if (!prefix.data) return false; + if (start < 0) start = 0; + if (start + prefix.length > length) return false; + return memcmp(data + start, prefix.data, prefix.length) == 0; + } + + // Check suffix + bool endswith(const str& suffix, __ss_int start = 0) const { + if (!suffix.data) return false; + if (start < 0) start = 0; + __ss_int effective_len = length - start; + if (suffix.length > effective_len) return false; + return memcmp(data + (length - suffix.length), suffix.data, suffix.length) == 0; + } + + // Find substring from start, return -1 if not found + __ss_int find(const str& sub, __ss_int start = 0) const { + if (!sub.data || sub.length == 0) return start < length ? start : length; + if (start < 0) start = 0; + for (__ss_int i = start; i + sub.length <= length; i++) { + if (memcmp(data + i, sub.data, sub.length) == 0) { + return i; + } + } + return -1; + } + + // Reverse find substring, return -1 if not found + __ss_int rfind(const str& sub, __ss_int start = 0) const { + if (!sub.data || sub.length == 0) return length; + if (start < 0) start = 0; + __ss_int begin = start; + if (begin > length) begin = length; + for (__ss_int i = length - sub.length; i >= begin; i--) { + if (memcmp(data + i, sub.data, sub.length) == 0) { + return i; + } + if (i == 0) break; + } + return -1; + } }; } // namespace shedskin diff --git a/stubs/__init__.py b/stubs/__init__.py new file mode 100644 index 0000000..2676101 --- /dev/null +++ b/stubs/__init__.py @@ -0,0 +1,2 @@ +# Make this directory a package for Shedskin/Python imports. + diff --git a/stubs/ros_mock.py b/stubs/ros_mock.py new file mode 100644 index 0000000..85f2db7 --- /dev/null +++ b/stubs/ros_mock.py @@ -0,0 +1,43 @@ +"""Minimal ROS stubs so Shedskin can convert ROS-dependent examples.""" + +class ROSInterruptException(Exception): + pass + +_shutdown = False + + +def init_node(name): + return None + + +def loginfo(msg): + print(msg) + + +class Publisher: + def __init__(self, topic, msg_type, queue_size=10): + self.topic = topic + + def publish(self, msg): + return None + + +class Subscriber: + def __init__(self, topic, msg_type, callback): + self.topic = topic + + +class TwistLinear: + def __init__(self): + self.x = 0.0 + + +class TwistAngular: + def __init__(self): + self.z = 0.0 + + +class Twist: + def __init__(self): + self.linear = TwistLinear() + self.angular = TwistAngular()