On aaarch-64, the address of a local dummy variable may be above a register save area in the stack frame, and thus the scan will miss some GC roots.
In TXR Lisp, I used to use a hacked constant on aarch64: STACK_TOP_EXTRA_WORDS. It wasn't large enough to straddle the area, and so operation on aarch64 was unreliable.
http://www.kylheku.com/cgit/txr/commit/?id=3aa731546c4691fac...
A good stack-top-getting trick occurred to me: call alloca for a small amount of memory and use that address. It has to be below everything; alloca cannot start allocating above some register save area in the frame, because then it would collide with it; alloca has not know the real stack top and work from there.
Since we need to scan registers, we use alloca for the size of the register file (e.g. setjmp jmp_buf), and put that there: kill two birds with one stone.
http://www.kylheku.com/cgit/txr/commit/?id=7d5f0b7e3613f8e8b...
Then use two stack frames! Every problem can be solved by adding an additional level of indirection. ;-)
0. We are already in a frame that doesn't take any arguments of the "val" object type; how come that's not good enough?
1. The current stack frame is entered with a bunch of callee-saved registers, some of which contain GC roots.
2. The current stack frame's code saves some of them: those ones that it clobbers locally. It leaves others in their original registers.
3. Thus, if a another stack frame is called, there are still some callee-saved registers, probably containing GC roots, and some of these will go into the area below the locals.
4. You might think that if the save all the necessary registers ourselves into the stack and then make another stack frame, we would be okay. But in fact, no. Because by the time we save registers, the compiler generated function entry has already executed and saved some of those registers into the below-locals save area and clobbered them for its own use! So our snapshot possibly misses GC roots. The compiler generated code always has "first dibs" at the incoming registers, to push them into the below-locals save area, thus kicking the GC roots farther up the stack.
"Can it be used in Production? It might be better to try Cello out on a hobby project first. Cello does aim to be production ready, but because it is a hack it has its fair share of oddities"
It sounds like they don't intend for it to be anything other than an interesting case study.
And the authors seem quite okay with that.
My sympathy and respect to the author, but they did not appear learn C well before trying to "fix" it. It is kind of irresponsible, I think, to say it "aims to be production ready" and write it up as something other C neophytes may be interested in with some of these issues.
Neither GCC's nor Clang's sanitisers pick up any undefined behaviour - and it's been like that for at least the last few years I've looked at it.
As to ignoring errors, and ignoring alignment, I don't think I've ever seen anything like that in the project. I have seen several pull requests delayed so that they will.
Overall, for what it's doing, this is one of the cleaner codebases I've dealt with.
It's pretty much exactly the same like the low-level-C techniques being instantly available for you in C++ or Objective C.
When I go look at a C code, I don't have to look up some annotation or new syntax that got introduced behind some abstraction that gets compiled in automatically after the library is pulled from the internet by whatever build system the project uses.
That's not the only reason, there is also simplicity, static typing and performance. If you favor the later two for whatever reason it can be used in places where you'd normal write a python/shell script or small program without too much extra effort (see https://github.com/RhysU/c99sh or suckless tools). Complexity is where Cello seems to fall down though, it seems like it introduces much more complexity than just using plain C with a decent "standard" library like glib.
I think the only meaningful benefit here is performance.
Simplicity is at best determined by the nature of the problem and at worst a completely subjective opinion for C.
Similarly, static typing is not usually something the programmer should care about that much. You need to know which paradigm your language uses, of course, but beyond that it does not matter all that much. IMX, you're more concerned with type safety, and C is not fully type safe like, say, Java is.
https://developer.gnome.org/glib/stable/glib-data-types.html
You don't get Cello's macros, and it uses reference counting instead of invisible garbage collection, but you get a lot of fun high-level capabilities.
It's a shame, in my opinion, that it never really received widespread love.
I think all serious work on it stopped about a decade ago.
Here's the original source tree, if you're interested: https://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/src/cmd...
https://github.com/orangeduck/Cello/blob/master/examples/ite...
Okay, so the vector is garbage-collectable once the function terminates ... but it has references to stack-allocated integers i0, i1 and i2. That leaves me wondering: won't the GC walk these and trample on stack memory that has been deallocated/reused.
(Maybe those integer values have a tag right in the val pointer that gets the GC to avoid dereferencing them.)
Raises the question of how usefully far you can make C twist using macros / preprocessor.
Candidates like Forth or Lisp seem possible. A few weekends at most. Might need to take a few liberties.
Python... Perhaps if you implement a less dynamic subset? Duck typing may trip you up. To what extent?
What about Elixir?
Code in the interpreter is directly converted to byte code, e.g. the macro Car generates the virtual machine instruction Car, rather than executing the code for car. The alternative would have been to generate byte code by hand, would have been error-prone. Here's the code for cons and let:
Define("cons", 2)
Local1 Local2 Cons Ret
Termin
DefineF("let")
Local1 Car
Local2 /* initialize new env */
Prog(1)
Ret
Params(2)
Until Local1 Null Do
Local1 Caar /* var */
Local1 Cadar Free12 Call("eval") /* val in old env */
Local2 /* env */
ACons
SetLocal2 Pop /* update new env */
PopLocal1
Od
Free11 Cdr Local2 Call("progn") /* use new env */
Ret
Termin
It is actually C, with heavy use of macros. But it can be read as Reverse Polish Lisp. It can also be thought of as a Lispy Forth.Brainf*ck is another classic.
var i0 = $(Int, 5);
vs int i0[5];
In both cases it doesn't need GC. What would be the reasons for redefining it? I wonder how it couples with local static variables.It's not higher level than C in the sense that you get any additional safety guarantees or real beneficial abstractions. If you are fine without the safety but want abstractions, use C++. If you want safety and abstractions, use Rust or Go or Zig. If you really want a transpile-to-C language, you've got Nim.
Finally, it's not good at being C; everything it does is poor practice and should be quickly recognized as such by experienced C developers, IMO. It's got no developer community and no real-world production consumers.
As the creator says, it’s not for production use. If I’m doing a side project, I’d give this a serious look.
True, but that's why we've got Rust these days. (Rust is actually more optimized than C, e.g. it will automatically reshuffle your structs to get rid of excess padding, and reference accesses will automatically take advantage of compiler-checked 'restrict' constraints, thus equalizing performance with e.g. FORTRAN.)
In contrast, the C language is fairly simple, except for a few twisty passages (pointer declaration syntax, anyone?). The standard library does leave something to be desired, but that's not that big of a deal given all the third party libraries out there.
It would be interesting to compare the same program written in straight C vs C++ vs Cello, both for developer experience issues (clarity, simplicity, etc.) and performance. I'll have to have a look at http://libcello.org/learn/benchmarks but this does really seem like something I'd like to use on a personal project someday.
Well, for a non-C language with high-level abstractions that lets me use C code relatively seamlessly - I'm content with C++. Many complain about its complexity, but you can actually avoid a lot of that complexity in _your_ code using facilities with complex implementation but relatively easy use.