What are you cooking?

Hey folks,

With AI helping to get things done quicker, I’m curious: has it changed the way you work with Crystal?

For me, it’s finally let me tackle an ambitious project I’ve had in mind for the better part of a decade but could never quite get off the ground.

Crystal hits a near-perfect balance between expressiveness, performance, and deployment simplicity (musl static builds with docker is braindead stupid to deploy). And compared to nine years ago, the main pain points I used to have with the language, mostly compile times, are largely gone now.

So, what are you all cooking?

For me it changed a lot.

Check my skuznetsov (Sergey Kuznetsov) · GitHub

Just my 2c: maybe if the scope of OP wasn’t narrowed to AI development with Crystal, then we could get more responses.

But considering that the main question is related to how AI is or isn’t changing the way a dev works with Crystal, in this case I would humbly suggest to change the title for this post to “What are you cooking with AI?”

My understanding is that there are many new projects being created in Crystal without AI with such importance (with AI just as an augmentation, or with no AI at all), and a new post of just “What are you cooking?” without AI involved in the conversation would probably received many more responses.

I suspect many users were happy to share what they are cooking now, but the AI ingredient changed the possibilities in the menu.

I think you read the intent a bit differently than I meant it.

The AI mention wasn’t the subject but just some “ice-breaker”. I just didn’t want to open with a flat what are you building with Crystal, felt a bit blunt, so I framed it around something that’s actually shifting in the industry right now.

Feel free to share what you are working on - even if AI has nothing to do with it.

But even the most conservative devs would agree that every project has parts that are pure implementation work, test suites, config, boilerplate, and that stuff moves a lot faster with AI, and this can lead to more ambitious one-dev projects. Hence my question.

If you’re tired of hearing about AI every single day (which I 100% get btw), that’s completely fair and a separate conversation :smile:.

If you even mention that you’re using AI on Reddit (in programming-related subreddits), you’ll get banned — they can’t tell the difference between good use of AI (for boilerplate, tests, documentation, etc.) and AI slop, so they just ban everything across the board.

Thanks for the nice response, @anykeyh. I am not against AI and also agree with what @kostya said: many don’t get the difference between good and bad use if AI.

Also thanks for the clarification.

Perhaps because what we see most often is the misuse of AI (a lot of slop, unfortunately), this somehow generates a contrary, though partly expected, response.

I’m working on an open-source cloud computing platform, but I confess that the need for efficiency with simplicity has made me to solve many underlying parts that people take for granted. Because of this, I’m “cooking” at least 3 important shards that will also exist as independent projects.

And I’ll share more info here on the forum when they’re more mature and presentable.

I always assumed I loved vibe coding — just handing everything over to AI and not worrying about the code. Turns out that’s not quite true.

A lot of the newer IDEs are built around chatting with an agent, and the part where you actually look at the source code has kind of faded into the background. Once I started using one of these, I noticed something weird about myself: even though I genuinely don’t read the code, I apparently can’t stand it when I can’t see the Git diff or browse the codebase after a change.

I don’t read the code. And it’s not like I’m worried about “losing my right” to dig into the details later, either. So this was a pretty unexpected thing to notice.

Online, you see a lot of people saying “I love AI, I never look at the code.” But now I’m kind of suspicious — do they really never look at it?

For me it’s more like flipping through a newspaper for a minute. What files got created, what the folder structure looks like, roughly how much changed, and — without really understanding the details — some vague sense of how complicated it all seems. I’m not reading it as text so much as looking at it like a pattern on a carpet, from a distance. Or maybe like watching a city slowly get built from above in a sim game.

So here’s the question I keep coming back to: does it actually mean anything to pick “Crystal” as the pattern on that carpet?

Lately I’ve been messing around with Go. I don’t like the language at all, but if AI is the one writing it, I honestly don’t care. As a language for “let AI write it, I’ll just vibe-check the pattern,” it’s actually pretty good. I’m using it purely to explore that idea — but deep down, I’m hoping the answer still turns out to be Crystal over Go.

(Translated from Japanese with Claude)

I’m personally not fan of those pure agentic framework like Claude Code or Codex, and use mostly AI assistance in IDE (Cline before, now Zed agent).

I prefer to review the code written by the AI, here is why:

An AI assistant is optimized to satisfy the given request. But the real input from a software engineer is not it’s capacity to assist by writing code or assessing the current situation, but should be seen as an arbiter.
An arbiter is optimized to satisfy the system, over time, against the request.

Those are different objectives, and the models are trained on the first.

You see it in the planning output. Ask for something robust and you get generality you did not need:

  • Abstractions with a single implementer
  • Configuration for options nobody will ever set

Every one of those choices can make sense locally, but broadly they produce a codebase nobody can hold in their head.

We say in French that “the best is the enemy of the good”.
The assistant will happily offer you the best option in planning mode.
A pure vibe coder will take it, and cripples its project.
Even if the AI assistant write perfectly functional code, you eventually scratch your head because you avoided trade-offs you should not have avoided.

The cost never shows up in a single decision, which is exactly why AI cannot catch it one decision at a time.

The other issue I can see is feature creep; using AI to build more than what it is needed at any given project time.

I’ve stumbled upon an interesting read by Caio Bianchi that touches upon the topic:

I’ve been working on my personal web framework for years, but using AI has considerably sped up its progress. I still review every line, but the whole development cycle is so much faster with tools like Codex. The latter has also gotten noticably better in matching exactly what I expect as the result over the last few months, which is as frightening as fascinating but here we are - it won’t go away anymore. I even started to try real vibe coding and not even looking at the code anymore for some throwaway tools, and they still work.

Maybe an even more important aspect is how AI enables me to do things I never could have done myself (create graphics/images), or that would have taken me weeks to get into (certain CSS/JS quirks, coding for domains I don’t know anything about, hell it even pulled some tricks in Crystal I couldn’t have come up with).

There is no doubt AI can be a very useful tool, and if you use it right, you can learn a lot from it (instead of just letting it do all the work alone). After all, it has basically all the knowledge the internet contains, and it will happily teach you anything you ask it for!

So what I’m currently “cooking” in my spare time is a little web game I’ve wanted to make for some years: https://rope.click. Using my own framework of course, and also meant as some kind of stress test for it to optimize performance bottlenecks. I could never have created those images myself, so this project really only exists because of AI - and I’m having the fun of my life building it.

I made a proof of concept JIT interpreter for Crystal, pretty much vibing: GitHub - vegai/crystal: The Crystal Programming Language · GitHub

My initial motivation was to be able to require “big” on the repl. That doesn’t currently work on real Crystal at least on my box because of (I think) linking issues against gmp.

Sort of interesting how easily the prototype happened. Making it production-ready would of course be a different story and would require actual competence and commitment. I have neither.

I would say that AI-assisted coding has helpd brainstorm and try certain theories without having to commit lot of time to discard or bring them back to proper design now that you’ve explored it.

As example, I recently compared Opus vs Fable at producing the same objective: add multi-dialect support to Drift, my pure-SQL migration library and CLI:

Found it interesting that Fable was able to come back multiple times with a good recommended approach (to test the non-deterministic situation to discard it was a fluke)

Opus 4.8 (high effort): Support other SQL dialects (MySQL, PostgreSQL) by luislavena · Pull Request #26 · luislavena/drift · GitHub

Fable 5 (high effort): Support MySQL and PostgreSQL databases by luislavena · Pull Request #32 · luislavena/drift · GitHub

Both were steered from the same prompt, but you can see the quality of the output and the PR description itself shows a bit more of the summarization capabilities from distilling the intent of the work.

Right now I’m exploring MyCompiler (from kostya) and created a subset of Crystal language that transpiles into C and gets compiled into standalone executable:

$ cat examples/fib.jasper
lib LibC
  fun printf(format : Pointer(UInt8), ...) : Int32
end

def fib(n : Int32) : Int32
  if n < 2
    return n
  end
  fib(n - 1) + fib(n - 2)
end

i = 0
while i < 10
  LibC.printf("fib(%d) = %d\n", i, fib(i))
  i = i + 1
end
$ bin/jasper build examples/fib.jasper

$ du -h fib
 36K    fib

$ file fib
fib: Mach-O 64-bit executable arm64

$ otool -L fib
fib:
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)

It has an error-tolerant lexer+parser (so it can output multiple diagnostic and recovery nodes without choking). There is no GC at all, just manual memory management:

lib LibC
  fun printf(format : Pointer(UInt8), ...) : Int32
end

class Point
  def initialize(@x : Int32, @y : Int32)
  end

  def length_squared : Int32
    @x * @x + @y * @y
  end
end

p = Point.new(3, 4)
LibC.printf("%d\n", p.length_squared)
p.release

But thinking on a refcounting approach instead (ala Swift) generated by the compiler analysis phases.

I will see how far it can go without macros, but quite interesting experience to also learn about these subjects.

Cheers.

wow and how? is it work for you?

I’m producing textual mycIR (.myc) from my AST (lowered to MyC semantics) and then using myc-c to get the generated the C file compiled with cc.

Found a few quirks on myc-c and its STDIN management, but since I’m going to move away from that and more into multiple .myc → .o generation + linking, so I can live with these quirks for the time being.

I haven’t tested the qbe or the llvm backends, only using the C one for now to keep things simple.

Keep you posted, just started two days ago (been designing it between work and family time and then let the agents code and validate it for me):

github.com/AlDanial/cloc v 2.10  T=0.01 s (2299.1 files/s, 361066.4 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Crystal                         21            338            145           2815
-------------------------------------------------------------------------------
SUM:                            21            338            145           2815
-------------------------------------------------------------------------------

And the supported Crystal syntax is quite limited.

Me too, but in the scale of weeks, so it’s not as far yet, and more experimental in nature. I’ve hit a bit of a roadblock in the form of documentation. A web framework needs good straightforward documentation, especially when it introduces new ways to do things, but my attempts at making Gemini do the work has ended up… Lacking. For starters, I don’t want the documentation to sound like Gemini, so I’ve ended up doing it manually for a start, to have something for it to learn from. But that’s been slow-going.

Documentation is definitely a part where I’m lagging behind as well. I’ve always been postponing this until “the API is stable” (haha!), but this is a chicken-and-egg situation: Fewer users/contributors means progress is slow. But without documentation, no new users. I’m not done learning this the hard way yet ;-)

I’m currently working on Meridian, a deployment tool similar to Kamal, but built around rootless Podman instead of Docker.

You don’t necessarily need a container registry either, since Meridian can also transfer and deploy images directly over SSH.

It uses kamal-proxy and supports blue-green deployments. It can also build and serve fingerprinted static assets from a separate subdomain as part of the deployment, so you don’t need a CDN.

I’m already using Meridian for some of my own projects. There is no 0.1.0 release yet, even though some text might read like there is a release, but I’m getting closer.

I’m making a command-line tool that displays random attractors in the terminal.
(It’s all AI vibe coding, though)

Q: Is it useful for anything?
A: No.

Q: Why did you make it?
A: I don’t know. I just thought I could make it.

With the arrival of the AI agents, I’ve been working on some foundational shards that I’ve always missed: a comprehensive AWS SDK, a fully featured CLI builder like clap (with things like auto-completion and man page generation), an abstraction over the file system, and things like that. Some of them I had already started before AI but never finished due to the sheer scope of them.

My idea is that with those, I can close the ecosystem gaps that prevented me from using crystal more pervasively at work. After I have finished them and used them enough in production, I’ll definitively be releasing them. Hopefully they’ll be useful for other people.

I was able to spend a few more hours steering some sessions and managed to get a few things done.

$ cloc src
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Crystal                         34            799           1626           7828
-------------------------------------------------------------------------------

  1. Naive ARC (Automatic Reference Counting, ala Swift) with --check-leaks option to instrument reference counting and validate things on program exit:
lib LibC
  fun printf(format : Pointer(UInt8), ...) : Int32
end

class Point
  def initialize(@x : Int32, @y : Int32)
  end
end

i = 0
while i < 3
  p = Point.new(i, i)
  i = i + 1
end
LibC.printf("done\n")
$ bin/jasper run --check-leaks examples/leaky.jasper
jasper: 3 live allocation(s) at exit
done                                    # exit 1
  1. Per-method incremental compilation: change a method, only that code gets recompiled. Relocate a method to another file, if no change to the method itself, no recompilation necessary:
# examples/wordcount.jasper
require "./tokenizer"

lib LibC
  fun printf(format : Pointer(UInt8), ...) : Int32
end

text = "the quick brown fox jumps over the lazy dog"
scanner = Tokenizer.new(text)
words = 0
longest = 0
length = scanner.next_word_size
while length >= 0
  words += 1
  longest = length if length > longest
  length = scanner.next_word_size
end
LibC.printf("%d words, longest %d\n", words, longest)
# examples/tokenizer.jasper
class Tokenizer
  def initialize(@text : String)
    @pos = 0
  end

  def space?(b : UInt8) : Bool
    b == 32_u8 || b == 10_u8 || b == 9_u8
  end

  # Length of the next word, or -1 when the text is exhausted.
  def next_word_size : Int32
    size = @text.size
    while @pos < size && space?(@text.byte_at(@pos))
      @pos = @pos + 1
    end
    return -1 if @pos >= size
    length = 0
    while @pos < size && !space?(@text.byte_at(@pos))
      length = length + 1
      @pos = @pos + 1
    end
    length
  end
end
$ bin/jasper run --stats examples/wordcount.jasper
Parse                        00:00:00.0008 (0.50MB)
Semantic: register classes   00:00:00.0000 (0.50MB)
Semantic: declare signatures 00:00:00.0001 (0.50MB)
Semantic: type check         00:00:00.0002 (0.50MB)
Semantic: flow analysis      00:00:00.0000 (0.50MB)
Semantic: hierarchy layout   00:00:00.0000 (0.50MB)
Semantic: checks             00:00:00.0000 (0.50MB)
Semantic: ARC synthesis      00:00:00.0000 (0.50MB)
Codegen (myc IR)             00:00:00.0019 (0.75MB)
Backend (myc)                00:00:00.0761 (1.44MB)
Cache                        2 hits, 13 misses (myc-qbe 24ce939d128c)
Link (cc)                    00:00:00.0274 (1.44MB)
9 words, longest 5
  1. Abstract classes, and basic inheritance, even with ivar coverage (to catch corner cases):
lib LibC
  fun printf(format : Pointer(UInt8), ...) : Int32
end

abstract class Shape
  def initialize(@name : String)
  end

  def name : String
    @name
  end
end

class Circle < Shape
  def initialize(value : Bool)
    return if value
    @name = "Circle"
  end
end

c = Circle.new(true)

# the next line is invalid memory in Crystal
LibC.printf("name=%s\n", c.name)

In Jasper does not even compile:

$ bin/jasper run 1.jasper
1.jasper:16:5: error: `Circle#initialize` must call `super` to initialize `@name`
      return if value
      ^^^^^^

But of course, nothing else for the time being, just playing with some theories, there is no cross-platform, stdlib, channels, fibers, not even unions :sweat_smile:

Just having some fun, learning a bit about those things that always was interested to see them at work and never had the opportunity to get to this point (only did 1 refactoring so far).

Cheers!