# C-Scripting *Allen Webster. June 28 2026* The only way forward is inside out. ### Index 1. Start C-Scripting in your C program. 2. What is C-Scripting? 3. How does it work? 4. Known pitfalls and unexplored treachery. 5. Introduction to the Symbol Set programming primitives. 6. Awesome things you can do with C-Scripting. 7. The end. ## 1. Start C-Scripting in your C program. 0. Pick a project of yours to integrate with - or start an empty "Hello World!" project to play with it. 1. Copy the files in the `symbol_set` folder into your project, add that folder to your includes path. 2. If you are on Linux compile `symbol_set.ld_meta`. The shell script `build_ld_meta.sh` builds this program with `clang`. You will want to be able to use the compiled program when you are building with `symbol_set` on Linux. 3. In any compilation unit where you are going to be C-Scripting, `#include "symbol_set.h"`. 4. Before the header, `#define SY__MAIN` -- If you have multiple compilation units, `#define SY__MAIN 1` into just one of them (perhaps `main.c`). At this point your program should build and behave as it did before setup. If something went wrong for you when you tried, you can contact `support@mr4th.com` and I can try to help you. If it does build, then you're ready to start C-Scripting! ## 2. What is C-Scripting? C-Scripting is when you're programming in C, and you've got an underlying architecture that lets you tap into features or expressivity that feel more scripting-like than C-like. I named this particular project "C-Scripting" but the spirit of the name goes back much futher. It's the idea that C doesn't have to be unproductive if you make smart decisions about how you equip yourself with tools. By looking for new ways to engage in C-Scripting, I have had two radical transformations to my style of C. The first came from embracing Arenas and non-conflicting local scratchs. This repository contains the kernel of my second radical transformation, a data structure I call a "Symbol Set". There are many ways to C-Script, but outside of this little discussion of the spirit of C-Scripting, *this* document is about C-Scripting with Symbol Sets. ### What is a Symbol Set? When you define a symbol set, you give it a name, and a type. Symbol sets live at global scope, and act like an enum and an array working together. The type given in the definition sets the type of the array elements. When you define a symbol, you specify which symbol set it belongs to. The symbol is assigned a unique id in the enum and a corresponding slot in the array. When you define a symbol, you define the static initialization for the corresponding slot in the array. Symbols often have names, but there are also great uses for unnamed symbols. Unnamed symbols still get an id and a corresponding slot in the array, they just can't be referenced statically by other code. Usually you do this when you have no reason to use the names and it would be tedious to have to come up with pointless unique names. You can imagine it looks sort of like this: ```C // define MyCommand type which I will underly the symbol set struct MyCommand{ String8 name; String8 description; Hook *hook; }; // define the MY_COMMAND symbol set SYMBOL_SET_DEFINE(MY_COMMAND, MyCommand); // define symbols in the MY_COMMAND symbol set (ie define commands) MY_COMMAND_DEFINE(Foo, "Do Foo."){ do_foo(ctx); } MY_COMMAND_DEFINE(EasyBar, "If it's not too hard, do Bar."){ if (difficulty(ctx) < 3){ do_bar(ctx); } } ``` Code structured very similarly to the above ends up with semantics that would be like having all that data, and all those function bodies organized into a single addressable table, and a corresponding enum: ``` void mycommand__Foo(void*){ do_foo(ctx); } void mycommand__EasyBar(void*){ if (difficulty(ctx) < 3){ do_bar(ctx); } } enum MY_COMMAND_ID{ MY_COMMAND_ID__NULL = 0, MY_COMMAND_ID__Foo, MY_COMMAND_ID__EasyBar, }; MyCommand MY_COMMAND_array[] = { { 0 }, { str8("Foo"), str8("Do Foo."), mycommand__Foo }, { str8("EasyBar"), str8("If it's not too hard, do Bar."), mycommand__EasyBar } }; ``` You can loop over the array of symbols. And you can statically reference the symbol names to get indices into the array, or just to compare ids, create run-time lists of symbols, etc. ## 3. How does it work? ### Implementing the Gather: Data Sections Under the hood, symbol sets are implemented by assigning each one a special data sections. Data sections exist in object files and executable files after you compile your code all the time, but usually we don't think about it. Typically there is a `.text` section that contains the executable code, a `.data` section that contains read-write data with non-zero initialized values, a `.rdata` section or `.rodata` for read only data, and a `.bss` section for read-write data that should be zero initialized, plus a handful of others that can contain information for other things like stack unwinding or debug information. Why use data sections for this? Because that's how you get the compiler to gather up all of the global variables in the same set, and lay them out into a single array. C doesn't have a feature that works this way on purpose. But any compiler that has an extension that lets you assign a global variable to a specific data section, already contains the necessary internal logic to achieve this feature, it just couples that logic to data sections. And that's why use data sections for this. ### Locating Data Sections In order for this to work, we have to be able to find the base pointer of the array, and either the count of symbols, or the pointer to the one-past-last slot in the array. #### Linux Version: Linker Scripting On Linux the solution I have arranged for this is the program `symbol_set.ld_meta`. The way this program works is it scans your object files before you link them, and it generates a linker script. Then when you link your program with all those object files you also pass the linker script, and it tells the linker how to generate symbols that mark the beginning and end of each section, and as a bonus it packs all the mini data sections down into the normal .data section. In order for the `symbol_set.ld_meta` program to know which data sections and symbols need to be arranged, I adopt the restriction that sections that implement symbol sets have to start with `.sy.`. This seems to me to be in keeping with the traditional way of using the name of the section to organize how it interacts with the "tool chain" programs. Even deeper under the hood, the `symbol_set.ld_meta` program builds a list of all the sections it sees that start with `.sy.` and also all the unresolved symbols it sees that look like markers for symbol set ranges (`syfirst__` and `syopl__` prefixes). The rest is a matter of generating a linker script like this: `sy.ld` ``` SECTIONS { .sy : { syfirst__cmd = .; *(.sy.cmd); syopl__cmd = .; } } INSERT AFTER .data; ``` #### Windows Version: Self Imaging On Windows the tool chain doesn't have anything as nice as a linker script. Instead we pay a little extra compute price on startup using a technique I call "Self Imaging". An "Image" in the context of executable formats and program loading is the "hot" version of the executable file. It's the version of the data that exists in the program's memory space, and on Windows our program's "Image" contains the section headers, and so we can find them at runtime. This is implemented in `symbol_set.h` under `Function Definitions`. And you don't even have to call it! You just include it, and a "Before Main" function calls it for you. The rest of the details are just parsing the PE/COFF headers. ### Resolving IDs: Pointer Arithmetic What we *want* is if we were to reference a symbol like `MY_COMMAND.Foo` that it would resolve to it's final id in the array. Unfortunately, we would need the linker to know about this and it doesn't. The linker can only patch up references to *addresses* because linkers don't have relocations for patching up indexes unfortunately. But that ability to patch up addresses gets us close to what we need. Recall, from C's perspective these "symbols" are just globals or static variables. So for example `MY_COMMAND_Foo` might be how you name the variable that stands for the `Foo` symbol of the `MY_COMMAND` symbol set. And therefore `&MY_COMMAND_Foo` is the address-of that variable. But that means, with the base pointer to the symbol set, we can do pointer arithmetic to get our IDs. Think `(&MY_COMMAND_Foo - MY_COMMAND__base)`. Unfortunately this means that static references to these IDs are not the low level constants we would want, they resolve to a subtract between a constant and a global variable. ## 4. Known pitfalls and unexplored treachery. There are a few common ways things can go wrong. The most distressing of which is that you can do everything right and still get a section that isn't the right size to reflect the number of symbols you actually created. There are a couple of ways this can happen that I have smoothed over so far. First, sometimes small types or types with small alignments don't get packed the way you'd expect. After all, it's not really a C language array, the standard doesn't say they have to be laid out in any particular way, but I have found I can ensure proper packing by using `SY__ALIGN_AS_LIT` on the declaration, another wrapper for non-standard C. Then I can just round the type size up to a multiple 8 to set the underlying stride for the array. The final interface hides this from you, but it's worth noting the loss of packing efficiency for small type sizes. Second, sometimes a bunch of null data will get appended to the end. This is "Incremental Linking". When you are building on Windows you have to pass `-INCREMENTAL:NO` to your linker to prevent this, or if you are using `clang` as a linker you pass `-Xlinker -INCREMENTAL:NO`. Symbols are not necessarily packed in the order you think, or at least I wouldn't recommend depending on it if it does. Again we're dealing with non-standard behavior of C, the compiler is free to arrange global variables however it wants, and symbol sets remain perfectly useful without having to depend on order. Symbol IDs are not serialization-ready on their own. There is no way in this scheme to ensure a symbol gets the same ID between builds. I have a number of ideas on how this can be addressed, but I haven't begun development on any of them yet, so if you start using symbol sets you're currently on your own for the serialization problem. When you reference the ID of a symbol, it is not considered a constant expression, because it's not until after linking that the pointer arithmetic could be resolved. But since the C compiler can't see this, it rejects uses of symbol ID references that occur in statically initialized data. This is too bad, because when symbols can reference other symbols, there are so many more powerful ways to use the system. I have a work around for this limitation in the `Raw` handle space for referencing symbols. See `SY_INIT` in section 5 for more on this. On Windows, your section names cannot be more than 8 characters long and I've already said you have to use 4 ".sy.". So you have to carefully allocate your 4 character section names when you make a new symbol set. On Windows, your sections are not compacted together so each takes up a minimum of 4K and is allocated in 4k pages separately. I have a number of candidates for ways to eliminate this either from all builds or at least from release builds, but I haven't developed any of those ideas yet. The usefulness of having symbols in small consecutive IDs is great, but it is constrained to singular compiled binaries (executables,.exe,.so,.dll). For instance, in a plugin system, each plugin would have it's own ID space, and therefore to reference a specific symbol you'd need to start using two-part IDs everywhere. So far I haven't developed anything to explore how this would work out. ## 5. Introduction to the Symbol Set programming primitives. In the file `symbol_set.h` the first section contains boilerplate for setting up a symbol set. The first 5 line block defines a symbol set. For instance if `MY_COMMAND` is the name of my symbol set, and `MyCommand` is the type, the code that instantiates the symbol set could be: ```C #define SYMBOL_SET_DEFINE MY_COMMAND #define MY_COMMAND_Type MyCommand #define MY_COMMAND_section ".sy.mcmd" #define MY_COMMAND_marker mcmd #include "symbol_set.define.h" ``` The next few blocks define various patterns of helper macros for defining symbols. The first block shows how I setup macros "data only" symbol types. ```C #define ZZZ_DEF(N,...) SyDefine(ZZZ, N) = { ... } ``` For instance you could use symbol sets to sketch out basic sprite metadata by hand. You could setup the definition helper macro like: ```C #define SPRITE_DEFINE(N,file,...) \ SyDefine(SPRITES, N) = { str8(file), __VA_ARGS__ } ``` And then you'd use it like: ```C // the SPRITE_DEFINE(Hero, "Hero.png", .x = 8, .y = 16); ``` The next block is how I setup macros for defining symbols with an attached function: ```C #define ZZZ_DEF(N,...) \ static void funczz##N(void*); \ SyDefine(ZZZ, N) = { funczz##N, ... }; \ static void funczz##N(void *ptr) ``` This is the version that all the "commands" example are using: ```C #define MY_COMMAND_DEFINE(N,desc) \ static void my_command__##N(Ctx*); \ SyDefine(ZZZ, N) = { str8(#N), str8(desc), my_command__##N }; \ static void my_command__##N(Ctx*) ``` The macro first declares the underlying C function, fills out the global variable initialization and inclues the C function, and then it sets up the signature of the function again, so that you can follow the macro with `{ ... }` the function block. ```C MY_COMMAND_DEFINE(Foo, "Do Foo."){ do_foo(ctx); } ``` I don't have a block for this in the boilerplate, but if you want to create a symbol type that wants multiple attached functions, you can do it like this: ```C #define ZZZ_DEF(N,...) \ static void func1zz##N(void*); \ static void func2zz##N(void*); \ SyDefine(ZZZ, N) = { func1zz##N, func2zz##N, ... }; #define ZZZ_FUNC1(N) void func1zz##N(void*) #define ZZZ_FUNC2(N) void func2zz##N(void*) ``` The final block shows how you can also setup symbols that don't have names. ```C #define ZZZ_DEF(...) SyDefineUnnamed(ZZZ) = { ... } ``` This is useful for cases where you're not really interested in the id space of the symbol set, only the collection of values you want to put together declaratively. This can be useful for creating optional attachments to named symbols, or structures that create relationships between named symbols. For instance, you could say one symbol set defines the set of "tags" you can put on another type of symbol, think `MY_COMMAND_TAG` for `MY_COMMAND`. Then you could use unnamed symbols to set these tags: ```C #define MY_COMMAND_ADD_TAG(CMD,TAG) \ SyDefineUnnamed(MY_COMMAND_TAG_LINKS) = { \ SyRaw(MY_COMMAND, CMD), SyRaw(MY_COMMAND_TAG, TAG) } ``` And then you would use it like: ```C MY_COMMAND_ADD_TAG(Foo,FooBar); MY_COMMAND_ADD_TAG(EasyBar,FooBar); MY_COMMAND_ADD_TAG(EasyBar,Easy); ``` you can then rearrange this data into a matrix or tags lists, or whatever you need during a simple startup processing phase. I don't really know any examples of unnamed symbols that don't rely on referencing a symbol id from another symbol, so I now need to explain how we can make this work. ### Inter-Symbol References First, we can't just store IDs into a symbol the way we'd want, because the data we store into symbols has to be statically resolved, but the IDs aren't resolvable until after linking. What we can do is use a variable address as a constant expression. Even though the final address of a global variable isn't resolved until link time, the compiler can count on the linker to fill in the gap with relocations, and so the compiler lets you treat that as a constant expression. (Take note, we need ID relocations to do better.) Instead what we do is we store the `Raw` for the symbol. The `Raw` is just the address of the global variable typed as a `SY__UAddr`. `SY__UAddr` is an address-widthed unsigned integer. We can use `SyIDFromRaw` at init time to translate all of the `Raw`s into `ID`s in place. It is for this reason that I put symbols in read-write memory even when I am treating them as strictly read-only after the ID fixup. I have developed a helper in `symbol_set.init.h` that I use for managing this. You can include this header after `symbol_set.h`. It sets up a symbol set called `SY_INIT` and each element of this set is a function pointer that describes how to fixup the symbol set. Using this helper, you define a symbol that references another symbol in the following way: ```C typedef struct MyThing{ SY__UAddr other_thing_id; } MyThing; // #define MY_THING_DEF(N,O) \ SyDefine(MY_THING, N) = { SyRaw(OTHER_THING, O) } #if SY__MAIN SY_INIT_DEF(MY_THING){ for (SyEach(MY_THING, thing)){ thing->other_thing_id = SyIDFromRaw(OTHER_THING, thing->other_thing_id); } } #endif ``` That is, I wrap the symbol reference in the helper macro to make all the symbol definitions easy on the eyes, and to ensure there that `SyRaw` is used and not `SyID`. Then if this is the compilation unit with `#define SY__MAIN 1`, I define a special `SY_INIT` symbol that describes how to loop over the symbol set and fixup `SyRaw`s into `SyID`s. I have to manually make sure the symbol set that I use in `SyRaw` (in the definition helper macro) matches the one I use in `SyIDFromRaw` (in the `SY_INIT` id fixup). If these don't match `SyIDFromRaw` just gives 0 ids. Then I just have to call `sy__run_init();` early in `main`. I don't use a run-before-main for this, because I already use run-before-mains to automate the process of locating symbol set arrays on Windows. These fixups can't be done until after symbols are located. `SY_INIT` is like run-before-main except you decide when to run all the `SY_INIT` functions explicitly, and therefore it can be timed strictly later than the symbol locating process. (In fact, because of all this, symbols shouldn't really be trusted during the "before main" execution phase, they should be considered ready to go at `main` and not necessarily before). This way of organizing the fixups makes the maintenance of lots of interconnected symbol sets a lot more manageable than it would be otherwise. I just consider the the ID fixup boilerplate a necessary component of defining the symbol set itself, and whenever I modify the symbol set types I try to be sure to recheck the ID fixup at the same time. ### Symbol Set Referencing Primitives After defining a `SYMBOL_SET` here are the primitives you can use that reference it. `SyType(SYMBOL_SET)` - Expands to the underlying type of the symbol set. `SyDeclare(SYMBOL_SET,N)` - Forward declares the symbol N. Like any other C forward declare you may do this as many times as you want for the same name, so long as you only define it once. This is useful when you want to reference a symbol before you've defined it. `SyDefine(SYMBOL_SET,N)` - Sets up the definition of the symbol N. The macro sets up the variable for the symbol, with the type from the `SYMBOL_SET`, and leaves the value unset, you finish the definition by writing ` = { ... };` after the macro. `SyDefineUnnamed(SYMBOL_SET)` - Works like `SyDefine` except you don't specify a name. `SyAddress(SYMBOL_SET,N)` - Gives the address of the symbol's slot in the set's array, typed as a pointer to the set's underlying type. Static resolution. `SyID(SYMBOL_SET,N)` - Gives the 1-based ID of symbol. Literally runtime resolution, theoretically more like link-time resolution. `SyRaw(SYMBOL_SET,N)` - Gives the address of the symbol typed as `SY__UAddr`. `SyFirst(SYMBOL_SET)` `SyOpl(SYMBOL_SET)` - Gives the address of the first and one-past-last slot of the symbol set's array. `SyStride(SYMBOL_SET)` - The stride between elements in the symbol set's array. `SyCount(SYMBOL_SET)` - The number of symbols in the symbol set. `SyNext(SYMBOL_SET,addr)` - Increments a pointer in the symbol set's array to the next element in the array (basically pointer arithmetic by the `SyStride`). `SyEach(SYMBOL_SET,var)` - Expands to the control phrases of a for loop: `for (SyEach(SYMBOL_SET,var)){ ... }`. `var` is a pointer to the set's underlying type, that iterates the whole array. `SyEachID(SYMBOL_SET,id)` - Expands to the control phrases of a for loop: `for (SyEachID(SYMBOL_SET,id)){ ... }`. `id` is a `U32` 1-based index that iterates the ids of the symbol set. `SyIDFromAddress(SYMBOL_SET,addr)` - Convert a pointer to a symbol to it's ID. `SyAddressFromID(SYMBOL_SET,addr)` - Convert an ID to a symbol pointer. `SyAddressCheck(SYMBOL_SET,addr)` - Evaluates true if the address is inside the range of the symbol set's array. `SyIDCheck(SYMBOL_SET,id)` - Evaluates true if the ID is one of the symbol IDs in the symbol set. `SyIDFromAddress_Unchecked` and `SyAddressFromID_Unchecked` - Like `SyIDFromAddress` and `SyAddressFromID`, but skips run-time checking the parameter is in a valid range, only use if you know you've got a valid address/ID. `SyIDFromRaw` and `SyIDFromRaw_Unchecked` same as `SyIDFromAddress`/`_Unchecked` except excepts `SY__UAddr` type instead of expecting pointer types. `SyFlag(SYMBOL_SET,N)` - Just think `(1 << SyID(SYMBOL_SET,N))` ## 6. Awesome things you can do with C-Scripting. ### Organized Command Systems Suppose you're building some kind of editor application where you have to organize a lot of editing commands into key bindings, and menus. Further imagine it could look like this: ```C EDITOR_COMMAND(InsertTodo, "Insert a TODO comment."){ U32 view_id = ed_current_view(ctx); U32 buffer_id = ed_buffer_from_view(ctx, view_id); U32 cursor_pos = ed_cursor_pos_from_view(ctx, view_id); ed_insert(ctx, buffer_id, cursor_pos, str8("// TODO: ")); } EDITOR_COMMAND_FLAGS(InsertTodo, ED_CmdFlag_Write); BIND_KEY(DefaultMap, ED_Key_T, ED_Mdfr_Alt, InsertTodo); ``` With a setup like this you could automatically assemble a statically defined keymap. And you could loop over all the commands you've written in your program, check their flags, string search and display their descriptions, and of course execute the command function if you have the parameters it needs. ### Organized Command Line Parameters Similarly you could use C-scripting to arrange your command line parsing. Imagine: ```C CMDLN_FLAG("start", "Specify the start time index for the scan. (default 0)", .duplicate = CMDLN_Behavior_Warning){ out->start = cmdln_read_u32(ctx); } CMDLN_FLAG("end", "Specify the end time index for the scan. (default end-of-timeline)", .duplicate = CMDLN_Behavior_Warning){ out->end = cmdln_read_u32(ctx); } CMDLN_FLAG("save", "Specify one or more intermediates to save.", .duplicate = CMDLN_Behavior_Concatenate){ U32 count = rpg_read_count(ctx); for (U32 i = 0; i < count; i += 1){ String8 intermediate_name = cmdln_read_idx_str8(ctx, i); U32 intermediate = timeline_intermediate_from_name(intermediate_name); if (intermediate != 0){ timeline_add_save(out, intermediate); } } } ``` ### State Machine You can set up a state machine programming system in a symbol set that looks like this: ```C typedef struct Ctx{ U32 state_id; } Ctx; /////////////////////////// STATE(GameSetup){ game_setup(ctx); state_next(ctx, SyID(STATE, GameScene1)); } STATE(GameScene1){ scene_character(ctx, 1, Left, SyID(CHARACTER, Hero)); scene_character(ctx, 2, Right, SyID(CHARACTER, DreamShadow)); scene_sound_effect(ctx, SyID(SOUND_EFFECT, Eerie)); scene_line(ctx, 1, "Huh? Is there someone there?"); // ... scene_line(ctx, 2, "Will you follow me into the deep?"); scene_offer_decision(ctx, 1, "Yes", 2, "No"); U32 decision = scene_player_decision(ctx); switch (decision){ case 1: { state_next(ctx, SyID(STATE, GameScene2Deep)); }break; case 2: { state_next(ctx, SyID(STATE, GameScene2Awake)); }break; } } ``` You can grab the symbol name in a macro with `#N` so we can save the symbol names as string data on symbols any time we'd like to, which means we can easily create an interface for running the state machine one step at a time and displaying the state, or logging each state as the machine runs, but a flat state machine is pretty limiting, so I recommend some upgrades. ### Stack Machine You can extend the state machine with a stack. When you do a state transition, you can also push a new state onto the stack. Alternatively you can pop the stack. ```C typedef struct Ctx{ U32 stack[100]; U32 stack_count; } Ctx; /////////////////////////// STATE(GameFight){ if (fight_exit_condition(ctx)){ state_pop(ctx); } else{ fight_step(ctx); state_stationary(ctx); } } STATE(GameSceneTutorialBoss){ setup_tutorial_boss(ctx); state_next_push(ctx, SyID(STATE, GameSceneTutorialFinished), SyID(STATE, GameFight); } ``` When you push a state, you also set a next state so that when you pop you don't necessarily have to go back to the state that did the push. This way state machines can be called like a subroutine. Now when you're logging what happens you could just show the top of the stack, which gives the same effect as stepping line by line in a debugger, but at the granularity of states. You can also "backtrace" this stack and see everything that has led up to the current state, because again it's so easy to connect readable names to these states and automatically print them out. So this above code says taht `GamesTutorialBoss` is a state that does some setup, and then it will appear to transition to `GameFight` but it does this by pushing `GameFight`, and at the same time, it sets `GameSceneTutorialFinished` as the next. This way `GameFight` can just pop and is generic and reusable for all sorts of contexts in the state machine, and doesn't need to hardwire itself for this particular relationship to `GameSceneTutorialFinished`. ### Stack Frames You can extend the state machine stack with typed frames that group states together and grant them access to shared variables. When you set the next state, the new state be a member of the same frame as the current state. But when you push a state, you may push any state, and you initialize the new frame that you push. ```C typedef struct StackNode{ struct StackNode *next; void *data; U32 state_id; U32 pop_to; } StackNode; typedef struct Ctx{ Arena *stack_arena; StackNode *stack; U32 stack_count; } Ctx; /////////////////////////// STACK_FRAME_STRUCT(Fight){ CharVars entities[100]; U32 entity_count; U32 player_id; F32 quick_time_cooldown; U32 quick_time_entity; }; STATE(FightStep, Fight){ B32 done = 0; U32 quick_times[100]; U32 quick_time_entities[100]; U32 quick_time_count = 0; // ... gather above variables from frame->entities ... if (done){ state_pop(ctx); } else if (quick_time_count > 0 && frame->quick_time_cooldown == 0){ U32 idx = rng_n(&ctx->rng, quick_time_count); frame->quick_time_cooldown = 5.f; frame->quick_time_entity = quick_time_entities[idx]; state_next(ctx, quick_times[idx]); } else{ frame->quick_time_cooldown = ClampBot(0.f, frame->quick_time_cooldown - ctx->dt); main_update(ctx, frame); state_stationary(ctx); } } STATE(QuickTimeZombieAttack, Fight){ qtime_before(ctx, 3.f); qtime_press(ctx, Button_Left); qtime_press(ctx, Button_Right); qtime_press(ctx, Button_Left); if (qtime_success(ctx)){ stun_chr(ctx, frame, frame->quick_time_entity); state_next(ctx, SyID(STATE, FightStep)); } else if (qtime_fail(ctx)){ hit_chr(ctx, frame, frame->player_id, 10); state_next(ctx, SyID(STATE, FightStep)); } } ``` Because you end up writing your own very simple main loop to run this state machine, and you implement the `state_*` transition functions, you can do things like track how long a state has run for, and make that information available to any of the state scripts `state_timer(ctx)` If you want you could then go further and attach other interesting things to the frames in this system, like self logging: ```C STACK_FRAME_LOG(Fight){ printf("entity_count = %u\n", frame->entity_count); printf("player = [%u] %E\n", frame->player_id, frame->entities + frame->player_id); printf("quick_time_cooldown = %f\n", frame->quick_time_cooldown); } ``` ### Organized Help Information Suppose you've got a lot of different kinds of symbols, perhaps you've got edit commands, signal filtering functions, file formats, etc. all modeled as symbol sets. Then say you decide that you'd like to start attaching more detailed help information to the symbols whose functioning is especially confusing. You can consider the following trick: ```C #define RuleClassXList(X) \ X(COMMAND) \ X(SIGNAL_FILER) \ X(FILE_FORMAT) typedef enum RuleClass{ RuleClass_NULL = 0, #define X(RC) RuleClass_##RC, RuleClassXList(X) #undef X RuleClass_COUNT } RuleClass; typedef struct Rule{ RuleClass rc; UAddr id; String8 text; } Rule; // #define RULE(RC,N,text) SyDefine(RC,N) = { .rc = RuleClass_##RC, .id = SyRaw(RC,N), .text = str8(text) } #if SY__MAIN SY_INIT(RULE){ for (SyEach(RULE,rule)){ switch (rule->rc){ #define X(RC) case RuleClass_##RC: rule->id = SyIDFromRaw(RC, rule->id); break; RuleClassXList(X) #undef X } } } #endif ``` With this setup, you can start attaching "rules text" to any named symbol so long as you add that symbol's symbol set to the list of rule classes. ## 7. The end. This is only the beginning. There's a final interpretation of the name C-Scripting that I chose for this project. It's also a reference to "C With Classes" the predecessor to C++. In a similar way C-Scripting is an exploratory project shaping something new out of the raw materials of C. But where I want to go next with this is not a programming language.