|
| 1 | +# EN.601.727 Machine Programming - Assignment 1 |
| 2 | + |
| 3 | +🎉 Welcome to your very first assignment in Machine Programming! |
| 4 | + |
| 5 | +In this journey, you’ll get your hands dirty with inductive program synthesis, starting with a bottom-up synthesizer, and ending with a taste of LLM-powered synthesis. |
| 6 | +Think of it as teaching a machine how to invent programs from scratch, and then inviting an AI assistant to join the fun. |
| 7 | + |
| 8 | +### ✨ Structure |
| 9 | + |
| 10 | +This assignment has three interconnected parts that gradually build on one another: |
| 11 | + |
| 12 | +- **Shapes DSL (Warm-up with Geometry)** |
| 13 | + Explore a small domain-specific language (DSL) for geometric shapes. You’ll implement a bottom-up synthesizer that automatically generates shape expressions based on positive and negative coordinates. |
| 14 | +- **Strings DSL (From Shapes to Strings)** |
| 15 | + Design a DSL for string manipulation—your own mini “string toolkit.” Then, reuse (and slightly adapt) your synthesizer from Part 1 to automatically generate string-processing programs. |
| 16 | +- **LLM-Assisted Synthesis (Humans + Machines)** |
| 17 | + Put a large language model (LLM) to work! Using the DSL you designed in Part 2, craft prompts that guide the LLM to synthesize string manipulation programs. Then, analyze what it gets right—and where it stumbles. |
| 18 | + |
| 19 | +### 📦 Deliverables and Submission |
| 20 | + |
| 21 | +You will implement several key functions for each part: |
| 22 | + |
| 23 | +- **Part 1: Bottom-up Synthesis for Shapes** |
| 24 | + - `shape_synthesizer.py`: `grow()` |
| 25 | + - `enumerative_synthesis.py`: `eliminate_equivalents()` |
| 26 | + - `enumerative_synthesis.py`: `synthesize()` |
| 27 | +- **Part 2: Bottom-up Synthesis for Strings** |
| 28 | + - `strings.py`: (add your new string operations) |
| 29 | + - `string_synthesizer.py`: `grow()` |
| 30 | +- **Part 3: LLM Synthesis for Strings** |
| 31 | + - `llm_string_synthesizer.py`: `generate_prompt()` |
| 32 | + - `llm_string_synthesizer.py`: `extract_program()` |
| 33 | + |
| 34 | +### 📌 Grading criteria: |
| 35 | + |
| 36 | +- **Parts 1 & 2**: Autograded. Full credit if you pass all tests within 30 minutes of runtime. |
| 37 | + (Hint: the reference solution runs most tasks in <1s, hardest ones in <10s.) |
| 38 | +- **Part 3**: Graded manually. Your LLM must solve at least 60% of test cases. |
| 39 | + Upload your llm_synthesis_report.json with all prompts/responses—it’s your proof of work. |
| 40 | + |
| 41 | +For Gradescope submission, zip the following 6 (or 7) files: |
| 42 | +- `strings.py` |
| 43 | +- `enumerative_synthesis.py` |
| 44 | +- `shape_synthesizer.py` |
| 45 | +- `string_synthesizer.py` |
| 46 | +- `llm_string_synthesizer.py` |
| 47 | +- `llm_synthesis_report.json` |
| 48 | +- (Optional) `readme.md` — for notes, acknowledgements, and AI/collaboration credits. |
| 49 | + |
| 50 | +### 🤝 Collaboration Policy |
| 51 | + |
| 52 | +You are encouraged to discuss ideas with peers. |
| 53 | +Do not copy code directly. |
| 54 | +Implement your own solution. |
| 55 | +If you collaborate (e.g., pair programming, brainstorming), credit your collaborators clearly in your `readme.md`. |
| 56 | + |
| 57 | +### 🤖 Using AI in This Assignment |
| 58 | + |
| 59 | +This is a Machine Programming course—of course you can use LLMs! |
| 60 | +LLMs can be great debugging partners, but they won’t give you a working solution right away. |
| 61 | +Prompt iteratively, and show that you understand the synthesis algorithms. |
| 62 | +Save interesting prompts + responses, and include them in your `readme.md`. |
| 63 | +Be explicit about which model you used. |
| 64 | + |
| 65 | +### 🔑 LLM API Key for Part 3 |
| 66 | + |
| 67 | +We’ll provide each of you with a Google Gemini API key for Part 3. |
| 68 | +The key is for this course only. |
| 69 | +Please do not share it, especially outside of the class. |
| 70 | +Typical usage for this assignment should not exceed $10. |
| 71 | +Excessive usage will be monitored, and we may revoke keys if abused. |
| 72 | + |
| 73 | +### 🧭 Integrity Guidelines |
| 74 | + |
| 75 | +- **Parts 1 & 2**: It’s fine to add smart heuristics in your DSL or synthesizer, but don’t hardcode answers to test cases—that defeats the purpose. |
| 76 | +- **Part 3**: Don’t fake the LLM’s output in your `.json` report. Both successes and failures are valuable learning outcomes in this course. |
| 77 | + |
| 78 | +### 📚 Reference |
| 79 | + |
| 80 | +The design of the synthesizer and the Shape DSL is adapted (with permission) from PSET1 in MIT’s Introduction to Program Synthesis, taught by Prof. Armando Solar-Lezama. |
| 81 | + |
| 82 | + |
| 83 | +Here’s a polished, engaging version of your **Setting up** section in markdown—clearer, a bit more fun, and student-friendly: |
| 84 | + |
| 85 | +# 🚀 Setting Up |
| 86 | + |
| 87 | +First things first—let’s get your environment ready. |
| 88 | + |
| 89 | +1. **Clone the repository** |
| 90 | + |
| 91 | + ```bash |
| 92 | + git clone https://github.com/machine-programming/assignment-1 |
| 93 | + ``` |
| 94 | + |
| 95 | +2. **Move into the assignment directory and install dependencies** |
| 96 | + |
| 97 | + ```bash |
| 98 | + cd assignment-1 |
| 99 | + pip install -r requirements.txt |
| 100 | + ``` |
| 101 | + |
| 102 | +3. **Test your setup (don’t panic if it fails!)** |
| 103 | + Run the tests for Part 1: |
| 104 | + |
| 105 | + ```bash |
| 106 | + pytest tests/test_shapes.py |
| 107 | + ``` |
| 108 | + |
| 109 | + You should see the tests run but **all of them fail**. |
| 110 | + ✅ That’s exactly what we expect—your job in Part 1 is to turn those failures into passes! |
| 111 | + |
| 112 | + |
| 113 | +# 🎨 Part 1: Bottom-up Synthesis for Shapes |
| 114 | + |
| 115 | + |
| 116 | + |
| 117 | +In this part, we’ll explore a Domain-Specific Language (DSL) for shapes. |
| 118 | +This DSL gives you a palette of basic shapes (rectangle, triangle, circle) and shape operations (union, intersection, mirror, subtraction). |
| 119 | + |
| 120 | +At its core, a shape $f$ is just a boolean function: |
| 121 | + |
| 122 | +$$f(x, y) \mapsto \texttt{true}~|~\texttt{false}$$ |
| 123 | + |
| 124 | +- `true` means that point $(x, y)$ falls within the shape $f$, |
| 125 | +- `false` means that the point $(x, y)$ falls outside of the shape $f$. |
| 126 | + |
| 127 | +### 🎯 Goal of synthesis |
| 128 | + |
| 129 | +Given a set of points with positive/negative labels (`List[Tuple[float, float, bool]]`), synthesize a shape program such that: |
| 130 | +- All positive points fall inside the shape |
| 131 | +- All negative points stay outside |
| 132 | + |
| 133 | +The image above shows an example with 12 positive and 12 negative points. |
| 134 | +The expected synthesized program was `Subtraction(Circle(5,5,4), Circle(5,5,2))`, which produces a ring. |
| 135 | + |
| 136 | +### 🧩 Understanding the DSL |
| 137 | + |
| 138 | +Shapes are implemented in shapes.py. |
| 139 | +The base class is `Shape`, which all concrete shapes inherit from. |
| 140 | +Each shape must implement the method `interpret(xs, ys)`, which takes two numpy arrays (`x` and `y` coordinates) and returns a boolean array. |
| 141 | + |
| 142 | +Example: the `Circle` class inherits from `Shape`: |
| 143 | + |
| 144 | +``` python |
| 145 | +class Circle(Shape): |
| 146 | + def interpret(self, xs: np.ndarray, ys: np.ndarray) -> np.ndarray: |
| 147 | + return ((xs - self.center.x)**2 + (ys - self.center.y)**2) <= self.radius**2 |
| 148 | +``` |
| 149 | + |
| 150 | +The `interpret` function computes whether each coordinate lies inside the circle (using vectorized numpy operations for speed). |
| 151 | + |
| 152 | +### 📜 Formal DSL Syntax |
| 153 | + |
| 154 | +``` |
| 155 | +Shape ::= Circle(center: Coordinate, radius: int) |
| 156 | + | Rectangle(bottom_left: Coordinate, top_right: Coordinate) |
| 157 | + | Triangle(bottom_left: Coordinate, top_right: Coordinate) # right triangle only |
| 158 | + | Mirror(Shape) # across line y=x |
| 159 | + | Union(Shape, Shape) |
| 160 | + | Intersection(Shape, Shape) |
| 161 | + | Subtraction(Shape, Shape) |
| 162 | +``` |
| 163 | + |
| 164 | +- **Terminals**: `Circle`, `Rectangle`, `Triangle` (with fixed parameters) |
| 165 | +- **Operators**: `Mirror`, `Union`, `Intersection`, `Subtraction` |
| 166 | + |
| 167 | +### 🔨 Part 1(a). Growing Shapes |
| 168 | + |
| 169 | +Time to roll up your sleeves! Head to `shape_synthesizer.py` and open the `ShapeSynthesizer` class. |
| 170 | +This synthesizer inherits from `BottomUpSynthesizer` but specializes in shapes. |
| 171 | +Your task is to implement `grow()`, which: |
| 172 | + |
| 173 | +- Takes a current set of shape programs. |
| 174 | +- Applies shape operators (union, subtraction, etc.) to generate new programs one level deeper. |
| 175 | +- Returns a set that includes both the original programs and the newly grown ones. |
| 176 | + |
| 177 | +Once implemented, your `grow()` function will be the engine that drives bottom-up search over the DSL, which step by step builds increasingly complex shapes. |
| 178 | + |
| 179 | +``` python |
| 180 | +def grow( |
| 181 | + self, |
| 182 | + program_list: List[Shape], |
| 183 | + examples: List[Tuple[float, float, bool]] |
| 184 | +) -> List[Shape]: |
| 185 | +``` |
| 186 | + |
| 187 | +> 💡 **Hints & Tips** |
| 188 | +> - Symmetry/commutativity: |
| 189 | +> Some operations (e.g., `Union(A, B) = Union(B, A)`) generate duplicate programs if you’re not careful. Add checks to prune equivalent programs. |
| 190 | +> - Progress tracking: |
| 191 | +> When you start generating large numbers of programs, visualization helps. Use `tqdm` to show a progress bar and keep your sanity. |
| 192 | +
|
| 193 | +### 🔨 Part 1(b). Eliminating (Observationally) Equivalent Shapes |
| 194 | + |
| 195 | +Now that you can **grow** shapes, the next challenge is to keep your search space from exploding. |
| 196 | +For this, we’ll turn to the more general `BottomUpSynthesizer` (in `enumerative_synthesis.py`) and implement a pruning step: **eliminating observationally equivalent programs**. |
| 197 | + |
| 198 | +Two programs are **observationally equivalent** if they produce the **same outputs** on the **same inputs**. For example, look at these two programs: |
| 199 | +* `Union(Circle(center=(0,0), r=1), Circle(center=(0,0), r=1))` |
| 200 | +* `Circle(center=(0,0), r=1)` |
| 201 | +These two programs are *different syntactically* but *indistinguishable observationally* (their outputs match on all test points). |
| 202 | + |
| 203 | +Your job is to filter out duplicates like these so the synthesizer only keeps *unique behaviors*. Please implement the `eliminate_equivalents` function: |
| 204 | + |
| 205 | +```python |
| 206 | +def eliminate_equivalents( |
| 207 | + self, |
| 208 | + program_list: List[T], |
| 209 | + test_inputs: List[Any], |
| 210 | + cache: Dict[T, Any], |
| 211 | + iteration: int |
| 212 | +) -> Generator[T, None, Dict[T, Any]]: |
| 213 | +``` |
| 214 | + |
| 215 | +* **`program_list`**: candidate programs to check |
| 216 | +* **`test_inputs`**: inputs on which programs will be interpreted |
| 217 | +* **`cache`**: a dictionary (`Dict[T, Any]`) mapping each program → its output signature (so you don’t recompute unnecessarily) |
| 218 | +* **`iteration`**: current synthesis round (useful for debugging/logging) |
| 219 | +* **Return**: a **generator** that yields only the *observationally unique* programs |
| 220 | + |
| 221 | +> 💡 Hints & Tips |
| 222 | +> - Use the provided `compute_signature()` method (already implemented) to evaluate programs and produce signatures. These signatures will be your deduplication keys. |
| 223 | +> - Keep track of which signatures you’ve already seen using `Set` or `Dict`. |
| 224 | +> Be careful: different programs may map to the *same* signature—yield only the first and discard the rest. |
| 225 | +> - **Important**: use `yield` instead of returning a list. This way, the synthesizer can stop early if it finds a successful program before exhausting the search space. |
| 226 | +> - The `cache` is your friend: store previously computed outputs there to save time when the same program shows up again. |
| 227 | +
|
| 228 | +### 🔨 Part 1(c). Bottom-up Synthesizing Shapes |
| 229 | + |
| 230 | +Now is the time to take all that we have already and iteratively synthesize shapes. |
| 231 | + |
| 232 | + |
| 233 | + |
| 234 | + |
| 235 | + |
| 236 | +# Part 2: Bottom-up Synthesis for Strings |
| 237 | + |
| 238 | + |
| 239 | + |
| 240 | +# Part 3: LLM Synthesis for Strings |
0 commit comments