Remove boxed dyn requirement on GBNF Limits.

This commit removes the use of dynamic dispatch for GBNF rule limits
by making use of some overly complicated generic trait bounds plus
associated types, along with two new types. Instead of wrapping GBNF
limits in a box dynamic trait object that produces GBNF limits, there
are two types: GbnfLimitedPrimitive and GbnfLimitedComplex.

These types contain the actual (not very complicated) logic to convert
their contained limit-values into the GBNF limits, which then get
converted to actual limits in the GBNF grammar. This removes heap
allocation and dynamic dispatch, as well as making creation of limit
objects more ergonomic.
This commit is contained in:
projectmoon 2024-03-10 14:39:32 +01:00
parent f2faf258c4
commit 001da3ca2f
8 changed files with 265 additions and 190 deletions

View File

@ -7,6 +7,7 @@ use crate::models::world::items::Item;
use crate::models::world::people::Person;
use crate::models::world::scenes::{Exit, Prop, Scene, Stage};
use crate::models::Insertable;
use gbnf::prelude::limit::{GbnfLimitedComplex, GbnfLimitedPrimitive};
use itertools::Itertools;
use strum::VariantNames;
use tabled::settings::Style;
@ -75,19 +76,6 @@ impl<'a> From<&'a Exit> for ExitTableRow<'a> {
}
}
const COMMAND_EXECUTION_BNF: &'static str = r#"
root ::= CommandExecution
CommandEvent ::= "{" ws "\"eventName\":" ws string "," ws "\"appliesTo\":" ws string "," ws "\"parameter\":" ws string "}"
CommandExecution ::= "{" ws "\"valid\":" ws boolean "," ws "\"reason\":" ws string "," ws "\"narration\":" ws string "," ws "\"event\":" ws CommandEvent "}"
CommandExecutionlist ::= "[]" | "[" ws CommandExecution ("," ws CommandExecution)* "]"
string ::= "\"" ([^"]*) "\""
boolean ::= "true" | "false"
ws ::= [ \t\n]*
number ::= [0-9]+ "."? [0-9]*
stringlist ::= "[" ws "]" | "[" ws string ("," ws string)* ws "]"
numberlist ::= "[" ws "]" | "[" ws string ("," ws number)* ws "]"
"#;
const COMMAND_EXECUTION_PROMPT: &'static str = r#"
[INST]
You are running a text-based adventure game. You have been given a command to execute. Your response must be in JSON.
@ -248,41 +236,25 @@ fn stage_info(stage: &Stage) -> String {
}
fn execution_gbnf_limit<'a>(stage: &'a Stage) -> RawCommandExecutionGbnfLimit {
// We will begin this implementation by simply setting the limits
// First version of this implementation is simply setting the limits
// to all uuids in the scene. We might wind up getting rid of the
// appliesTo field, leaving only parameter behind.
let people_keys = stage
.people
.iter()
.map(|p| &p._key)
.cloned()
.flatten();
let item_keys = stage
.items
.iter()
.map(|i| &i._key)
.cloned()
.flatten();
let curr_scene_key = stage.scene._key.iter();
let people_keys = stage.people.iter().map(|p| &p._key).cloned().flatten();
let item_keys = stage.items.iter().map(|i| &i._key).cloned().flatten();
let exit_keys = stage.scene.exits.iter().map(|e| &e.scene_key).cloned();
let all_uuids = people_keys
.chain(item_keys)
//.chain(curr_scene_key)
.chain(exit_keys)
.collect_vec();
let all_uuids = people_keys.chain(item_keys).chain(exit_keys).collect_vec();
let mut applies_to = all_uuids.clone();
applies_to.push("self".to_string());
//let all_uuids = std::rc::Rc::new(all_uuids);
let event_limit = RawCommandEventGbnfLimit {
applies_to: Box::new(all_uuids.clone()),
parameter: Box::new(all_uuids.clone()),
applies_to: GbnfLimitedPrimitive::new(applies_to),
parameter: GbnfLimitedPrimitive::new(all_uuids),
};
RawCommandExecutionGbnfLimit {
event: Box::new(event_limit),
event: GbnfLimitedComplex::new(event_limit),
}
}
@ -299,9 +271,6 @@ pub fn execution_prompt(original_cmd: &str, stage: &Stage, cmd: &ParsedCommand)
let limit = execution_gbnf_limit(stage);
let grammar = RawCommandExecution::to_grammar_with_limit(limit);
println!("{}", grammar);
//AiPrompt::new_with_grammar_and_size(&prompt, COMMAND_EXECUTION_BNF, 512)
AiPrompt::new_with_grammar_and_size(&prompt, &grammar, 512)
}

View File

@ -2,7 +2,10 @@ use ai::logic::AiLogic;
use anyhow::Result;
use config::Config;
use game_loop::GameLoop;
use models::world::scenes::{root_scene_id, Stage};
use models::{
commands::RawCommandExecutionGbnfLimit,
world::scenes::{root_scene_id, Stage},
};
use state::GameState;
use std::{io::stdout, rc::Rc, str::FromStr, time::Duration};

View File

@ -1,7 +1,5 @@
use std::fmt::Display;
use gbnf::prelude::*;
use gbnf_derive::{Gbnf};
use gbnf_derive::Gbnf;
use serde::{Deserialize, Serialize};
use strum::{EnumString, EnumVariantNames};
use thiserror::Error;
@ -63,7 +61,7 @@ pub struct RawCommandExecution {
pub reason: Option<String>,
pub narration: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[gbnf_limit]
#[gbnf_limit_complex]
pub event: Option<RawCommandEvent>,
}
@ -82,9 +80,9 @@ impl RawCommandExecution {
#[serde(rename_all = "snake_case")]
pub struct RawCommandEvent {
pub event_name: String,
#[gbnf_limit]
#[gbnf_limit_primitive]
pub applies_to: String,
#[gbnf_limit]
#[gbnf_limit_primitive]
pub parameter: String,
}

View File

@ -4,24 +4,31 @@ use std::{
ops::{Deref, DerefMut},
};
use itertools::Itertools;
use limited::{Limited, LimitedGbnfComplex, LimitedGbnfField, LimitedGbnfPrimitive};
use serde::de::DeserializeOwned;
use crate::limits::convert::{Limited, LimitedGbnfComplex, LimitedGbnfField, LimitedGbnfPrimitive};
use convert_case::{Case, Casing};
use itertools::Itertools;
use serde::de::DeserializeOwned;
mod limited;
mod limits;
pub mod limit {
pub use crate::limits::definitions::*;
}
pub mod prelude {
pub use crate::gbnf_field;
pub use crate::gbnf_field_type;
pub use crate::limit;
pub use crate::limit::*;
pub use crate::AsGbnf;
pub use crate::AsGbnfComplex;
pub use crate::AsGbnfLimit;
pub use crate::AsGbnfPrimitive;
pub use crate::AsGrammar;
pub use crate::GbnfComplex;
pub use crate::GbnfField;
pub use crate::GbnfFieldType;
pub use crate::GbnfLimit;
pub use crate::GbnfLimitedField;
pub use crate::GbnfPrimitive;
pub use crate::GbnfRule;
pub use crate::GbnfRuleLevel;
@ -35,106 +42,8 @@ pub mod prelude {
/// complex types). This allows the creation of a complex nested Limit
/// struct which can hold the full structure of nested limits of
/// multiple types that derive Gbnf and have limits.
pub trait GbnfLimitedField<T> {
fn limit(self: Box<Self>) -> GbnfLimit;
}
impl<T: GbnfLimitedField<T> + 'static> From<Box<T>> for Box<dyn GbnfLimitedField<T>> {
fn from(value: Box<T>) -> Self {
value
}
}
impl<T: ToString + AsGbnfPrimitive + 'static> From<Vec<T>> for Box<dyn GbnfLimitedField<T>> {
fn from(value: Vec<T>) -> Self {
Box::new(value)
}
}
impl<T, const N: usize> From<[T; N]> for Box<dyn GbnfLimitedField<T>>
where
T: ToString + AsGbnfPrimitive + 'static,
{
fn from(value: [T; N]) -> Self {
Box::new(value)
}
}
impl<T: ToString + AsGbnfPrimitive + 'static> From<Box<Vec<T>>> for Box<dyn GbnfLimitedField<T>> {
fn from(value: Box<Vec<T>>) -> Self {
value
}
}
// Single pritive field types that can only have specific values.
impl<T: ToString + AsGbnfPrimitive> GbnfLimitedField<T> for Vec<T> {
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
T::to_gbnf_primitive(),
self.into_iter().map(|v| v.to_string()).collect(),
)
}
}
impl<T: ToString + AsGbnfPrimitive> GbnfLimitedField<std::rc::Rc<T>> for Vec<T> {
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
T::to_gbnf_primitive(),
self.into_iter().map(|v| v.to_string()).collect(),
)
}
}
// List types that can only have specific values.
impl<T: ToString + AsGbnfPrimitive> GbnfLimitedField<Vec<T>> for Vec<T> {
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
T::to_gbnf_primitive(),
self.into_iter().map(|v| v.to_string()).collect(),
)
}
}
impl<T, const N: usize> GbnfLimitedField<T> for [T; N]
where
T: ToString + AsGbnfPrimitive,
{
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
T::to_gbnf_primitive(),
self.into_iter().map(|v| v.to_string()).collect(),
)
}
}
impl<T, const N: usize> GbnfLimitedField<Vec<T>> for [T; N]
where
T: ToString + AsGbnfPrimitive,
{
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
T::to_gbnf_primitive(),
self.into_iter().map(|v| v.to_string()).collect(),
)
}
}
impl<'a> GbnfLimitedField<String> for &'a [&'a str] {
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
String::to_gbnf_primitive(),
self.into_iter().map(|v| v.to_string()).collect(),
)
}
}
impl<'a> GbnfLimitedField<String> for std::rc::Rc<Vec<&'a str>> {
fn limit(self: Box<Self>) -> GbnfLimit {
GbnfLimit::Simple(
String::to_gbnf_primitive(),
self.iter().map(|v| v.to_string()).collect(),
)
}
pub trait AsGbnfLimit {
fn to_gbnf_limit(self) -> GbnfLimit;
}
/// A type of GBNF value limit, either simple (a list of values meant
@ -147,12 +56,6 @@ pub enum GbnfLimit {
Complex(HashMap<&'static str, GbnfLimit>),
}
impl<T> From<Box<dyn GbnfLimitedField<T>>> for GbnfLimit {
fn from(value: Box<dyn GbnfLimitedField<T>>) -> Self {
value.limit()
}
}
// Converts GBNF defintions (through the types below) into the grammar
// rules.
pub trait AsGrammar {
@ -180,6 +83,10 @@ pub trait AsGbnfPrimitive {
fn to_gbnf_primitive() -> GbnfPrimitive;
}
pub trait AsGbnfComplex {
fn to_gbnf_complex() -> GbnfComplex;
}
macro_rules! define_field_type {
($type:ty, $gbnf_type:expr) => {
impl AsGbnf for $type {
@ -331,6 +238,8 @@ impl GbnfRule {
)
}
/// Turn this rule into an option rule: main rule is an option type, and
/// dependent rule is the original rule itself.
pub fn to_optional(self) -> GbnfRule {
let option_token = self.name.option_token();
let option_rule_text = format!(r#"{} | "null""#, self.name);
@ -340,7 +249,7 @@ impl GbnfRule {
}
/// Turn this rule into a list rule: main rule is a list type, and
/// dependent rule is the original type/rule itself.
/// dependent rule is the original rule itself.
pub fn to_list(self) -> GbnfRule {
let list_name = self.name.list_token();
let list_rule_text =
@ -354,7 +263,7 @@ impl GbnfRule {
list_rule
}
/// Consume this rule to produce a list of rules consisting of
/// Consume this rule to produce an ordered list of rules consisting of
/// this rule and its dependents. The rules in the list have no
/// dependents. Useful for final output of a rules list.
fn flatten(mut self) -> Vec<GbnfRule> {

View File

@ -4,6 +4,7 @@ use crate::{
};
use itertools::Itertools;
#[derive(Debug, Clone)]
pub struct LimitedGbnfComplex<'a> {
pub complex: &'a GbnfComplex,

View File

@ -0,0 +1,157 @@
use crate::AsGbnfComplex;
use crate::AsGbnfLimit;
use crate::AsGbnfPrimitive;
use crate::GbnfLimit;
pub trait GbnfLimitType {
type Type;
}
pub trait GbnfLimitStructMarker {}
macro_rules! define_limit_marker {
($field_type:ty, $value_type:ty) => {
impl GbnfLimitType for $field_type {
type Type = $value_type;
}
};
}
define_limit_marker!(String, String);
define_limit_marker!(i32, i32);
impl<T> GbnfLimitType for Vec<T>
where
T: GbnfLimitType,
{
type Type = <T as GbnfLimitType>::Type;
}
impl<T> GbnfLimitType for Option<T> {
type Type = T;
}
pub trait GbnfLimitTypeContainer<T: GbnfLimitType> {
type ContainerType;
}
impl GbnfLimitTypeContainer<String> for String {
type ContainerType = Vec<String>;
}
impl GbnfLimitTypeContainer<i32> for i32 {
type ContainerType = Vec<i32>;
}
impl<T: GbnfLimitType> GbnfLimitTypeContainer<T> for Vec<T> {
type ContainerType = Vec<<T as GbnfLimitType>::Type>;
}
impl<T: GbnfLimitType + GbnfLimitTypeContainer<T>> GbnfLimitTypeContainer<Vec<T>> for Vec<T> {
type ContainerType = <T as GbnfLimitTypeContainer<T>>::ContainerType;
}
impl<T: GbnfLimitType> GbnfLimitTypeContainer<T> for Option<T> {
type ContainerType = <T as GbnfLimitType>::Type;
}
impl<T: GbnfLimitType> GbnfLimitTypeContainer<Option<T>> for Option<T> {
type ContainerType = <T as GbnfLimitType>::Type;
}
pub struct GbnfLimitedPrimitive<T>
where
T: GbnfLimitType + GbnfLimitTypeContainer<T>,
{
field_type: std::marker::PhantomData<T>,
values: <T as GbnfLimitTypeContainer<T>>::ContainerType,
}
impl<T> GbnfLimitedPrimitive<T>
where
T: GbnfLimitType + GbnfLimitTypeContainer<T>,
{
pub fn new(values: <T as GbnfLimitTypeContainer<T>>::ContainerType) -> GbnfLimitedPrimitive<T> {
GbnfLimitedPrimitive {
field_type: std::marker::PhantomData,
values,
}
}
}
impl<T> AsGbnfLimit for GbnfLimitedPrimitive<T>
where
T: AsGbnfPrimitive
+ ToString
+ GbnfLimitTypeContainer<T, ContainerType = Vec<T>>
+ GbnfLimitType<Type = T>,
{
fn to_gbnf_limit(self) -> GbnfLimit {
let vals = self.values.into_iter().map(|v| v.to_string());
GbnfLimit::Simple(<T as AsGbnfPrimitive>::to_gbnf_primitive(), vals.collect())
}
}
impl<T> AsGbnfLimit for GbnfLimitedPrimitive<Vec<T>>
where
T: AsGbnfPrimitive
+ ToString
+ GbnfLimitTypeContainer<T, ContainerType = Vec<T>>
+ GbnfLimitType<Type = T>,
Vec<T>: GbnfLimitType,
{
fn to_gbnf_limit(self) -> GbnfLimit {
let vals = self.values.into_iter().map(|v| v.to_string());
GbnfLimit::Simple(<T as AsGbnfPrimitive>::to_gbnf_primitive(), vals.collect())
}
}
pub struct GbnfLimitedComplex<T>
where
T: GbnfLimitType + GbnfLimitTypeContainer<T>,
{
field_type: std::marker::PhantomData<T>,
values: <T as GbnfLimitTypeContainer<T>>::ContainerType,
}
impl<T> GbnfLimitedComplex<T>
where
T: GbnfLimitType + GbnfLimitTypeContainer<T>,
{
pub fn new(values: <T as GbnfLimitTypeContainer<T>>::ContainerType) -> GbnfLimitedComplex<T> {
GbnfLimitedComplex {
field_type: std::marker::PhantomData,
values,
}
}
}
impl<T, U> AsGbnfLimit for GbnfLimitedComplex<T>
where
T: AsGbnfComplex + GbnfLimitType<Type = U> + GbnfLimitTypeContainer<T, ContainerType = U>,
U: AsGbnfLimit + GbnfLimitStructMarker,
{
fn to_gbnf_limit(self) -> GbnfLimit {
self.values.to_gbnf_limit()
}
}
impl<T, U> AsGbnfLimit for GbnfLimitedComplex<Vec<T>>
where
T: AsGbnfComplex + GbnfLimitType<Type = U> + GbnfLimitTypeContainer<T, ContainerType = U>,
U: AsGbnfLimit + GbnfLimitStructMarker,
{
fn to_gbnf_limit(self) -> GbnfLimit {
self.values.to_gbnf_limit()
}
}
impl<T, U> AsGbnfLimit for GbnfLimitedComplex<Option<T>>
where
T: GbnfLimitType<Type = U> + GbnfLimitTypeContainer<T, ContainerType = U>,
U: AsGbnfLimit + GbnfLimitStructMarker,
{
fn to_gbnf_limit(self) -> GbnfLimit {
self.values.to_gbnf_limit()
}
}

2
gbnf/src/limits/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub(crate) mod convert;
pub mod definitions;

View File

@ -3,11 +3,11 @@ use darling::{FromDeriveInput, FromField};
use proc_macro::{Span, TokenStream};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens};
use syn::parse_macro_input;
use syn::{parse_macro_input, Attribute};
use syn::{DeriveInput, Ident, LitStr};
#[derive(Clone, Debug, FromField)]
#[darling(forward_attrs(gbnf_limit))]
#[darling(forward_attrs(gbnf_limit_primitive, gbnf_limit_complex))]
struct GbnfFieldDef {
ident: Option<syn::Ident>,
ty: syn::Type,
@ -15,14 +15,47 @@ struct GbnfFieldDef {
attrs: Vec<syn::Attribute>,
}
enum LimitType {
LimitedPrimitive,
LimitedComplex,
NotLimited,
}
impl GbnfFieldDef {
fn limit_type(&self) -> LimitType {
let limit_priv = self
.attrs
.iter()
.find(|attr| attr.path().get_ident().unwrap().to_string() == "gbnf_limit_primitive");
let limit_complex = self
.attrs
.iter()
.find(|attr| attr.path().get_ident().unwrap().to_string() == "gbnf_limit_complex");
match (limit_priv, limit_complex) {
(Some(_), None) => LimitType::LimitedPrimitive,
(None, Some(_)) => LimitType::LimitedComplex,
(Some(_), Some(_)) => panic!("cannot be both a primitive and complex limit"),
(None, None) => LimitType::NotLimited,
}
}
}
impl ToTokens for GbnfFieldDef {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let ident = &self.ident;
let ty = &self.ty; // TODO figure out how to get ident out
let vis = &self.vis;
let wrapper_type = match self.limit_type() {
LimitType::LimitedPrimitive => quote! { GbnfLimitedPrimitive<#ty> },
LimitType::LimitedComplex => quote! { GbnfLimitedComplex<#ty> },
_ => panic!("somehow attempting to make a limited field without a helper attr"),
};
let output = quote! {
#vis #ident: Box<dyn GbnfLimitedField<#ty>>
#vis #ident: #wrapper_type
};
output.to_tokens(tokens);
@ -55,13 +88,18 @@ impl ToTokens for GbnfStructDef {
}
}
fn is_limited_field(attr: &Attribute) -> bool {
let ident = attr.path().get_ident().unwrap().to_string();
ident == "gbnf_limit_primitive" || ident == "gbnf_limit_complex"
}
/// Find fields in the struct with a #[gbnf_limit] attribute.
fn find_limited_fields(fields: &[GbnfFieldDef]) -> impl Iterator<Item = &GbnfFieldDef> + '_ {
fields.iter().filter(|field| {
field
.attrs
.iter()
.find(|attr| attr.path().get_ident().unwrap().to_string() == "gbnf_limit")
.find(|attr| is_limited_field(attr))
.is_some()
})
}
@ -72,7 +110,7 @@ fn find_non_limited_fields(fields: &[GbnfFieldDef]) -> impl Iterator<Item = &Gbn
field
.attrs
.iter()
.find(|attr| attr.path().get_ident().unwrap().to_string() == "gbnf_limit")
.find(|attr| is_limited_field(attr))
.is_none()
})
}
@ -95,18 +133,28 @@ fn generate_to_grammar_impl(original_struct_name: &Ident, fields: &[GbnfFieldDef
.to_token_stream();
let ident = &field.ident;
let value = quote! { self.#ident.into() };
let value = quote! { self.#ident.to_gbnf_limit() };
quote! { (#key, #value) }
});
let as_gbnf_complex_impl = quote! {
impl AsGbnfComplex for #original_struct_name {
fn to_gbnf_complex() -> GbnfComplex {
Self::to_gbnf().as_complex()
}
}
};
if limit_struct_fields.len() > 0 {
quote! {
pub struct #limit_struct_name {
#(#limit_struct_fields),*
}
impl #limit_struct_name {
pub fn to_gbnf_limit(self) -> GbnfLimit {
impl GbnfLimitStructMarker for #limit_struct_name {}
impl AsGbnfLimit for #limit_struct_name {
fn to_gbnf_limit(self) -> GbnfLimit {
GbnfLimit::Complex(
HashMap::from([
#(#from_assignments),*
@ -115,28 +163,12 @@ fn generate_to_grammar_impl(original_struct_name: &Ident, fields: &[GbnfFieldDef
}
}
impl GbnfLimitedField<#original_struct_name> for #limit_struct_name {
fn limit(self: Box<#limit_struct_name>) -> GbnfLimit {
self.to_gbnf_limit()
}
impl GbnfLimitType for #original_struct_name {
type Type = #limit_struct_name;
}
impl GbnfLimitedField<Option<#original_struct_name>> for #limit_struct_name {
fn limit(self: Box<#limit_struct_name>) -> GbnfLimit {
self.to_gbnf_limit()
}
}
impl From<#limit_struct_name> for Box<dyn GbnfLimitedField<#original_struct_name>> {
fn from(limit: #limit_struct_name) -> Self {
Box::new(limit)
}
}
impl From<#limit_struct_name> for Box<dyn GbnfLimitedField<Option<#original_struct_name>>> {
fn from(limit: #limit_struct_name) -> Self {
Box::new(limit)
}
impl GbnfLimitTypeContainer<#original_struct_name> for #original_struct_name {
type ContainerType = #limit_struct_name;
}
impl #original_struct_name {
@ -145,6 +177,8 @@ fn generate_to_grammar_impl(original_struct_name: &Ident, fields: &[GbnfFieldDef
Self::to_gbnf().as_complex().to_grammar(Some(gbnf_limit))
}
}
#as_gbnf_complex_impl
}
} else {
quote! {
@ -155,6 +189,8 @@ fn generate_to_grammar_impl(original_struct_name: &Ident, fields: &[GbnfFieldDef
GRAMMAR.get_or_init(|| Self::to_gbnf().as_complex().to_grammar(None))
}
}
#as_gbnf_complex_impl
}
}
}
@ -241,7 +277,7 @@ fn map_limited_gbnfs(fields: &[GbnfFieldDef]) -> impl Iterator<Item = TokenStrea
}
/// Convert a Rust type into a GBNF grammar.
#[proc_macro_derive(Gbnf, attributes(gbnf_limit))]
#[proc_macro_derive(Gbnf, attributes(gbnf_limit_primitive, gbnf_limit_complex))]
pub fn gbnf(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
generate_gbnf(&input)