Comment by newzino
Thanks for the detailed response. The interpreted approach makes sense for the use case - when you're embedding a scripting layer, you usually want simplicity and portability over raw speed anyway.
The aliasing semantics you describe (resizes propagating through aliases) is an interesting choice. It's closer to how references work in languages like Python than to the "borrow checker" approach Rust takes. Probably more intuitive for users coming from dynamic languages, even if it means some operations need runtime checks.
The hybrid GC approach (reference counting + periodic tracing) is pragmatic. Reference counting handles the common case cheaply, and the tracing pass catches cycles. That's similar to how CPython handles it.
The C registration API sounds clean - explicit pairing of names to function pointers is about as simple as it gets. Do you handle varargs on the Cicada side, or does each registered function have a fixed arity that the interpreter enforces?
Yes there are lots of runtime checks.. unfortunately, but I always fork the time-consuming calculations into C anyway so those checks don’t really affect overall performance much.
Scripted functions have no set arity, and the same applies to callback C functions. Scripted functions collect their arguments inside an ‘args’ variable. Likewise, each C function has a single ‘argsType’ argument which collects the argument pointers & type info, and there are macros to help unpack them but if you want to do the unpacking manually then the function can be called variadically:
ccInt myCfunction(argsType args)
{ for (int a = 0; a < args.num; a++) printf(“%p\n”, args.p[a]); return 0; }
So all functions are automatically variadic.
It’s good to know that these GC/etc. solutions are even used by the big languages..