Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env

This file was deleted.

22 changes: 12 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,34 +76,26 @@ jobs:
# Start a local llama-cpp server via devenv, then run the counter
# example for 30s. This exercises the full pipeline: dylib creation,
# compilation, loading, dispatch, LLM evolution, and hot-swap.
# Exit code 124 = timeout killed the process (success).
run: |
nix develop --command bash -c '
devenv up -d
devenv processes wait llama-server --timeout 120
timeout 30s cargo run -p counter-example || [ $? -eq 124 ]
cargo run -p counter-example
devenv processes down
'
- name: fizzbuzz-example-smoke-test
env:
RUSTFLAGS: "-D warnings"
# Run the fizzbuzz example which evolves a function to pass a test
# suite. Timeout after 60s — the example exits on its own once all
# tests pass or after max_rounds.
run: |
nix develop --command bash -c '
devenv up -d
devenv processes wait llama-server --timeout 120
timeout 60s cargo run -p fizzbuzz-example || [ $? -eq 124 ]
cargo run -p fizzbuzz-example
devenv processes down
'
- name: rastrigin-example-smoke-test
env:
RUSTFLAGS: "-D warnings"
# Run the rastrigin example which evolves a function to match
# sample data via symbolic regression. Timeout after 120s — the
# example exits on its own once the formula is found or after
# max_rounds.
run: |
nix develop --command bash -c '
devenv up -d
Expand All @@ -124,3 +116,13 @@ jobs:
timeout 180s cargo run -p sort-example || [ $? -eq 124 ]
devenv processes down
'
- name: struct-support-example-smoke-test
env:
RUSTFLAGS: "-D warnings"
run: |
nix develop --command bash -c '
devenv up -d
devenv processes wait llama-server --timeout 120
cargo run -p struct-support-example
devenv processes down
'
13 changes: 11 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"examples/quantize",
"examples/rastrigin",
"examples/sort",
"examples/struct-support",
"examples/tictactoe",
"symbiont",
"symbiont-macros",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ symbiont::evolvable! {

#[tokio::main]
async fn main() -> symbiont::Result<()> {
let runtime = symbiont::Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Debug).await?;
let runtime = symbiont::Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let agent = symbiont::inference::init_agent()?;
let fn_sigs = runtime.fn_sigs();
let base_prompt = format!(
Expand Down
7 changes: 5 additions & 2 deletions examples/counter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

info!("SYMBIONT_DECLS: {SYMBIONT_DECLS:#?}");
let runtime = Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Debug).await?;
let runtime = Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let fn_sigs = runtime.fn_sigs(); // Alternatively, `fn_full_sources` can be used to also show doc string and default function body.
info!("fn_sigs: {fn_sigs:?}");

Expand All @@ -45,7 +45,7 @@ async fn main() -> symbiont::Result<()> {
let mut last_evolution = std::time::Instant::now();
let evolution_interval = Duration::from_secs(5);

loop {
for _ in 0..10 {
step(&mut counter);
println!("counter: {counter}");
std::thread::sleep(Duration::from_secs(1));
Expand All @@ -61,4 +61,7 @@ async fn main() -> symbiont::Result<()> {
last_evolution = std::time::Instant::now();
}
}
assert!(counter > 10);

Ok(())
}
2 changes: 1 addition & 1 deletion examples/fizzbuzz/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ fn run_tests() -> (usize, usize, String) {
async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

let runtime = Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Debug).await?;
let runtime = Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let fn_sigs = runtime.fn_sigs();
info!("fn_sigs: {fn_sigs:?}");

Expand Down
3 changes: 2 additions & 1 deletion examples/quantize/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ fn plot_frontier_progression(
async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

let runtime = Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Release).await?;
let runtime =
Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Release).await?;
let fn_sigs = runtime.fn_sigs();
info!("fn_sigs: {fn_sigs:?}");

Expand Down
2 changes: 1 addition & 1 deletion examples/rastrigin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ fn sample_table(samples: &[Sample]) -> String {
async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

let runtime = Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Debug).await?;
let runtime = Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let fn_sigs = runtime.fn_sigs();
info!("fn_sigs: {fn_sigs:?}");

Expand Down
3 changes: 2 additions & 1 deletion examples/sort/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ fn total_time(results: &[BenchResult]) -> Duration {
async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

let runtime = Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Release).await?;
let runtime =
Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Release).await?;
let fn_sigs = runtime.fn_sigs();
info!("fn_sigs: {fn_sigs:?}");

Expand Down
15 changes: 15 additions & 0 deletions examples/struct-support/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
edition = "2024"
license = "MPL-2.0"
name = "struct-support-example"
publish = false
version = "0.1.0"

[lints]
workspace = true

[dependencies]
symbiont = { path = "../../symbiont" }

tokio.workspace = true
tracing.workspace = true
66 changes: 66 additions & 0 deletions examples/struct-support/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MPL-2.0
//! The example shows support for functions taking in custom structs from the surrounding scope.

use symbiont::Runtime;
use tracing::info;

/// A 2D game state, with just the x and y coordinates.
///
/// Annotated with `#[symbiont::shared]` so the macro records the type's
/// source code into a hidden `__SYMBIONT_SHARED_GameState` constant that
/// `evolvable!` can pull into the dylib via `shared GameState;`.
#[symbiont::shared]
#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[allow(dead_code, reason = "Just debug impl is used.")]
struct GameState {
/// The x coordinate. Range of 0..100
x: usize,
/// The y coordinate. Range of 0..250
y: usize,
}

symbiont::evolvable! {
// Bring the externally-defined `GameState` into the dylib's source so the
// evolved function below can reference it.
shared GameState;

/// Implement some different logic in here, while respecting the bounds laid out in the docs.
fn step(state: &mut GameState);
}

#[tokio::main]
async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

info!("SYMBIONT_DECLS: {SYMBIONT_DECLS:#?}");
let runtime = Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let fn_prelude = runtime.fn_prelude();
let fn_source = runtime.fn_full_sources();
info!("fn_prelude: {fn_prelude:#?}, fn_source: {fn_source:#?}");

let agent = symbiont::inference::init_agent()?;

let base_prompt = format!(
"Give an implementation for this evolvable function:\n
```
{:#?}
{:#?}\n
```.\n",
fn_source[0], fn_prelude[0],
);
runtime
.evolve(&agent, &base_prompt)
.await
.expect("Can successfully evolve");
info!(
"Successfully evolved the function, which is now hot-reloaded in-place. Next call to `step` will run the newly compiled Agent code."
);

let mut state = GameState::default();

step(&mut state);
println!("state: {state:?}");
assert_ne!(state, GameState::default(), "Game state must have evolved.");

Ok(())
}
2 changes: 1 addition & 1 deletion examples/tictactoe/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ fn overall_score(results: &[MatchResult]) -> f64 {
async fn main() -> symbiont::Result<()> {
symbiont::init_tracing();

let runtime = Runtime::init(SYMBIONT_DECLS, symbiont::Profile::Debug).await?;
let runtime = Runtime::init(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let fn_sigs = runtime.fn_sigs();
info!("fn_sigs: {fn_sigs:?}");

Expand Down
2 changes: 1 addition & 1 deletion symbiont-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ keywords = ["agent", "dylib", "dynamic", "harness", "llm"]
license = "MPL-2.0"
name = "symbiont-macros"
repository = "https://github.com/MathisWellmann/symbiont"
version = "0.7.0"
version = "0.8.0"

[lints]
workspace = true
Expand Down
64 changes: 58 additions & 6 deletions symbiont-macros/src/evolvable.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use syn::{
ForeignItemFn,
Item,
ItemFn,
Signature,
Visibility,
parse::{
Parse,
ParseStream,
},
punctuated::Punctuated,
};

syn::custom_keyword!(shared);

/// A single function declaration inside `evolvable! { ... }`.
///
/// Supports two forms:
Expand Down Expand Up @@ -42,25 +46,73 @@ impl EvolvableFn {
}
}

/// The contents of an `evolvable! { ... }` block: zero or more function declarations.
/// The contents of an `evolvable! { ... }` block.
///
/// Holds zero or more function declarations plus a "prelude" of supporting
/// items. The prelude has two complementary forms:
///
/// - **Inline items**: structs, enums, type aliases, `use` statements, etc.
/// declared directly inside the macro body. Re-emitted in both the host
/// crate and the dylib.
/// - **Shared references**: `shared Foo, Bar;` lines that pull in items
/// annotated with `#[symbiont::shared]` from outside the macro. Each
/// reference resolves to a `__SYMBIONT_SHARED_<Ident>` const containing
/// the source of the referenced item.
#[expect(clippy::field_scoped_visibility_modifiers, reason = "Good here")]
pub(crate) struct EvolvableBlock {
pub(crate) functions: Vec<EvolvableFn>,
/// Non-fn items declared inline inside the macro.
pub(crate) prelude_items: Vec<Item>,
/// Idents referenced via `shared <Ident>, <Ident>; ` lines. Each maps
/// to a `__SYMBIONT_SHARED_<Ident>` constant produced by the
/// `#[symbiont::shared]` attribute macro.
pub(crate) shared_refs: Vec<syn::Ident>,
}

impl Parse for EvolvableBlock {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut functions = Vec::new();
let mut prelude_items = Vec::new();
let mut shared_refs = Vec::new();

while !input.is_empty() {
// Try parsing as a full function (with body) first
// `shared Foo, Bar;` — references to items annotated with
// `#[symbiont::shared]` outside the macro body.
if input.peek(shared) {
input.parse::<shared>()?;
let idents: Punctuated<syn::Ident, syn::Token![,]> =
Punctuated::parse_separated_nonempty(input)?;
input.parse::<syn::Token![;]>()?;
shared_refs.extend(idents);
continue;
}

// Try parsing as a full function (with body) first.
let fork = input.fork();
if fork.parse::<ItemFn>().is_ok() {
functions.push(EvolvableFn::WithBody(input.parse::<ItemFn>()?));
} else {
// Fall back to bodyless (foreign-style) declaration
functions.push(EvolvableFn::WithoutBody(input.parse::<ForeignItemFn>()?));
continue;
}

// Try parsing as a bodyless function declaration.
let fork = input.fork();
if fork.parse::<ForeignItemFn>().is_ok() {
let parsed: ForeignItemFn = input.parse()?;
functions.push(EvolvableFn::WithoutBody(parsed));
continue;
}

// Fall back to any other top-level item (struct/enum/type/use/...).
let item: Item = input.parse()?;
match item {
Item::Fn(f) => functions.push(EvolvableFn::WithBody(f)),
other => prelude_items.push(other),
}
}
Ok(EvolvableBlock { functions })
Ok(EvolvableBlock {
functions,
prelude_items,
shared_refs,
})
}
}
Loading
Loading