Excellent question. Introducing a gpio_proxy.rs is a fantastic architectural decision that significantly improves the transpiler's design. This is a classic software engineering pattern known as a Facade or Adapter, and it creates a clean "impedance match" between the high-level concepts of your scripting language and the low-level details of the hardware abstraction layer.
You are correct: this proxy would mirror the Starlark gpio.star API in Rust, acting as a stable bridge to the potentially complex or changing esp-hal implementation.
This approach introduces a new layer: the Target Abstraction Layer (TAL).
The gpio_proxy.rs file will define a set of structs and functions that are:
- Domain-Specific: The API will look like the Starlark API (e.g.,
Pin::new(13, "output"),pin.high()), not like theesp-halAPI (Output::new(io.pins.gpio13, ...)). - A Stable Target: The transpiler (
build.rs) will now generate code that only calls functions ingpio_proxy.rs. It no longer needs to know anything aboutesp-halitself. - The Bridge: The proxy's implementation contains all the specific, potentially messy logic for calling
esp-hal, handling pin ownership, dealing withunwrap()s, etc.
This decouples the "what" (the logic in Starlark) from the "how" (the specific hardware calls in esp-hal).
This file defines the stable API that our transpiler will target.
// src/gpio_proxy.rs
use esp_hal::gpio::{self, AnyPin, GpioPin, Input, Level, Output, Pull};
use esp_hal::prelude::*;
// An enum to hold the specific, type-safe `esp-hal` pin driver.
// This allows our GpioProxy to manage both inputs and outputs.
pub enum PinMode<'d> {
Output(Output<'d, AnyPin<'d>>),
Input(Input<'d, AnyPin<'d>>),
}
// The proxy struct itself. It owns the configured `esp-hal` pin object.
pub struct GpioProxy<'d> {
number: u8,
mode: PinMode<'d>,
}
// This is the implementation of our stable, high-level API.
impl<'d> GpioProxy<'d> {
/// Creates a new output pin. This mirrors the Starlark `Pin(num, OUT)` call.
pub fn new_output(
io_pins: &'d mut gpio::Pins<'d>,
pin_number: u8,
initial_level: Level,
) -> Self {
// All the `esp-hal` specific logic is encapsulated here.
// The transpiler doesn't need to know about `degrade`.
let pin = GpioPin::new(io_pins, pin_number).degrade();
let output = Output::new(pin, initial_level);
GpioProxy {
number,
mode: PinMode::Output(output),
}
}
/// Sets the output pin level to HIGH. Mirrors `pin.high()`.
pub fn high(&mut self) {
if let PinMode::Output(pin) = &mut self.mode {
// The proxy handles the Result/unwrap logic.
pin.set_high().unwrap();
} else {
// In a real implementation, you might panic or log an error.
}
}
/// Sets the output pin level to LOW. Mirrors `pin.low()`.
pub fn low(&mut self) {
if let PinMode::Output(pin) = &mut self.mode {
pin.set_low().unwrap();
}
}
// You would add `new_input`, `value()`, etc. here to fully mirror the API.
}The transpiler's logic is now dramatically simplified. It doesn't need to construct complex esp-hal calls; it just maps Starlark calls directly to GpioProxy calls.
// build.rs (TranspilationAgent logic)
// ... inside the transpile_statement function ...
let token_stream = match stmt.node {
// Match assignment: `led = gpio.Pin(13, mode=gpio.OUT)`
starlark::syntax::Stmt::Assign(assign) => {
let var_name = assign.lhs.node.to_string();
// Simplified parsing for brevity...
let pin_number: u8 = 13; // Parsed from Starlark AST
let var_ident = Ident::new(&var_name, Span::call_site());
let pin_lit = syn::Literal::u8_unsuffixed(pin_number);
// BEFORE (targeting esp-hal directly):
// quote! { let mut #var_ident = Output::new(io.pins.gpio13.degrade(), Level::Low); }
// AFTER (targeting the stable GpioProxy API):
quote! {
let mut #var_ident = GpioProxy::new_output(&mut io.pins, #pin_lit, Level::Low);
}
}
// Match a method call statement: `led.high()`
starlark::syntax::Stmt::Expression(expr) => {
// Simplified parsing...
let obj_name = "led";
let method_name = "high";
let obj_ident = Ident::new(&obj_name, Span::call_site());
let method_ident = Ident::new(&method_name, Span::call_site());
// BEFORE:
// quote! { #obj_ident.set_high().unwrap(); }
// AFTER (a clean 1:1 mapping):
quote! {
#obj_ident.#method_ident();
}
}
_ => quote! {},
};
// ... rest of the transpiler ...
```The transpiler is now cleaner, more readable, and completely insulated from the details of `esp-hal`.
### Step 3: Updating `main.rs`
The main application file simply needs to bring the proxy into scope.
```rust
// src/main.rs
#![no_std]
#![no_main]
// ... other use statements ...
use esp_hal::gpio::{Io, Level};
// Include the proxy module
mod gpio_proxy;
use gpio_proxy::GpioProxy;
#[entry]
fn main() -> ! {
// ... standard setup ...
let mut io = Io::new(peripherals.GPIO, peripherals.IO_MUX);
// This generated code now calls our `GpioProxy`, not `esp-hal` directly.
include!(concat!(env!("OUT_DIR"), "/transpiled_script.rs"));
loop {}
}This new component fundamentally improves the architecture. Here's how to update the AGENTS.md document.
To enhance modularity and maintainability, the system architecture introduces a crucial new component: the Target Abstraction Layer (TAL), implemented in gpio_proxy.rs. This layer acts as a stable bridge between the code generated by the transpiler and the low-level hardware drivers (esp-hal).
The revised data flow is as follows:
graph TD
A[Starlark Script (.star)] --> B{Parsing Agent};
B --> C[Starlark AST];
C --> D{Transpilation Agent};
D --> E[syn AST (Targeting TAL API)];
E --> F{Code Generation Agent};
F --> G[Generated Rust Code (.rs)];
G -- "Calls API of" --> H[Target Abstraction Layer (gpio_proxy.rs)];
H -- "Encapsulates calls to" --> I[Hardware Abstraction Layer (esp-hal)];
The Transpilation Agent's responsibility is updated. It no longer needs knowledge of the underlying esp-hal primitives.
- Input: A Starlark
AstModulestruct. - Process: Translates Starlark statements into
synAST structures that exclusively call the public API defined by the Target Abstraction Layer (gpio_proxy.rs). - Output: A
Vec<syn::Stmt>containing calls likeGpioProxy::new_output(...)andled.high().
This is the bridge component that isolates the transpiler from the hardware details.
- Agent Name:
GpioProxy - Implementation:
src/gpio_proxy.rs - Responsibility:
- Expose a Stable, High-Level API: It provides a Rust API that mirrors the concepts of the Starlark
gpio.starmodule (e.g.,new_output,high,low). - Encapsulate HAL Complexity: It contains all the implementation details for interacting with
esp-hal, including pin ownership, type-state management, error handling (unwrap()), and hardware-specific function calls (.degrade()).
- Expose a Stable, High-Level API: It provides a Rust API that mirrors the concepts of the Starlark
- Benefits:
- Decoupling: The transpiler (
build.rs) is completely decoupled fromesp-hal. Ifesp-halundergoes a breaking API change, only thegpio_proxy.rsimplementation needs to be updated. - Maintainability: The concerns are separated. The transpiler handles language translation; the proxy handles hardware interaction.
- Portability: To target a different microcontroller (e.g., an STM32), a new proxy implementation could be written that conforms to the same TAL API, requiring zero changes to the transpiler itself.
- Decoupling: The transpiler (