Kube.ist needs an app description I can read:

app "web" {
  chart = "example/web"
  replicas = 3
}

I like Terraform's named blocks and its dependency model. They fit how I picture infrastructure better than Ansible's task-by-task playbooks. The way it looks matters to me too.

I'm exploring Terraform-like GitOps. I want to describe the application in one place and see which resources an update will touch. Somewhere between Terraform and Kubernetes manifests, with less to untangle than an over-complicated Helm setup.

I think developers are more familiar with string interpolation and the normal syntax of Terraform than they are with the weird YAML nightmare of Helm.

The configuration I want

Terraform uses state to keep track of the resources its configuration manages, and it also keeps dependency information needed when a resource disappears from the configuration. By stateless, I mean avoiding that separate resource record. The infrastructure still has state. I'd read it from the systems I'm managing and compare it with the declaration in Git, but I still need to work out how to find the right resource reliably. Leaving out Terraform's separate record gives me that problem to solve. That matters most for things that have to outlive the application. Forgetting to describe a disk and intending to erase it are two different requests.

An application can use the network without managing its lifetime. An application update should have a small, clear set of resources it may change. The separate applies in the polder system give that rule a practical starting point.

I started with HCL. It supplies the grammar and parser used by Terraform, while I define the blocks and attributes my application understands. I can read the parsed blocks and check their values.

HCL reads the configuration. Terraform supplies the resource and state management. I can use HCL's parser for a different runtime, including the stateless idea.

What I get by borrowing

A wise man would take HCL's parser, let it worry about forgotten quotes and get on with the application.

Luckily, I am not a wise man, as it's too much fun not to do it myself. My own little grammar would let me set the rules for names and strings. So I'm starting with a lexer, the bit that turns characters into tokens. Names, spaces, escaped quotes. I can get far too interested in how a string is spelt before I've done much with the application it's for.

From characters to checked settings

Let's follow the opening line:

app "web" {

A lexer groups characters into tokens. Here it produces a name, a string and an opening brace:

IDENT   app      at 1:1
STRING  "web"    at 1:5
LBRACE  {        at 1:11

Whitespace separates the name from the string, but the space inside a quoted string would belong to that string. The scanner therefore needs to know whether it's reading quoted text or the surrounding configuration. Seeing a quote changes the rules until the matching closing quote arrives. That is the purpose of a lexer mode. In normal mode a brace can be punctuation, while inside a string it can be an ordinary character and an escape sequence may change what the next quote means. The scanner is following the chosen string rules, rather than asking every later stage to rediscover them.

For this example, each token keeps its starting position:

type Token struct {
    Kind   Kind
    Text   string
    Line   int
    Column int
}

The position tells me where the token came from. Two identical strings can be on different lines, and a useful error needs to point to the right one.

The original spelling matters too. An escape can occupy several characters in the file but become one character in the decoded value. I'd keep both. An error should show the text somebody wrote, and I'm already holding the original.

Remove the closing quote from the application name:

app "web {

The scanner starts a string at column five, and because this model requires it to close on the same line, reaching the line ending is an error. It can point straight to the opening quote because it kept that position.

Now restore the quote and change the replica count:

app "web" {
  replicas = "three"
}

Every token is perfectly recognisable. There is a name, an equals sign and a complete string. A parser can also recognise the attribute assignment, and the problem appears when application validation asks for a numeric replica count and receives a string.

A forgotten quote calls for “unterminated string”. Supply the wrong value, and “replicas expects a number” tells me what to fix. Those are different mistakes, even if both stop the file from working. A lexer that rejected every string after replicas would be learning application policy it shouldn't need to know.

I'd like the error to name the application and attribute, then point at the value I typed. The validator knows which application we're checking. The lexer kept the position. I can use both to tell somebody exactly where replicas = "three" went wrong.

Scanning the valid block produces IDENT app at 1:1, STRING "web" at 1:5 and LBRACE at 1:11. The parser recognises an app block named web with replicas = 3. Application checks accept the numeric count. A space or escaped quote inside a string stays within one token. An unclosed string stops scanning at 1:5. Removing the equals sign leaves valid tokens, but the parser cannot recognise the assignment. With replicas = "three", scanning and parsing succeed. Application checks reject the string at 2:14 because replicas expects a number.
Figure 1 - Reading a configuration. Choose an example and use Step to watch characters become tokens, a block and checked settings. Try Missing equals or Wrong type to see where the stages stop. Edit source lets you change the input.

The explorer runs a teaching scanner, parser and application checks. Try removing the equals sign too. The scanner can still recognise every token, but the parser can no longer make an assignment from them.

What I'm taking on

Taking over the frontend gives me these jobs:

QuestionBorrow the frontendOwn the frontend
Where does a string end?Use the language's ruleDefine and implement the rule
How are source positions tracked?Keep the provided rangesRecord them while scanning
Which syntax can a user write?Work within the existing grammarMaintain the chosen grammar
What does replicas accept?Apply application validationApply application validation

The tool still has Helm values to produce and an application plan to work out. Resources to find, references, decisions about when to delete something.... For now, I'm working on the lexer.

Suppose I add a reference such as database.host. The lexer may only need a dot token. The parser needs to recognise a reference expression. Then I still have to work out which database that name means and when its host is available. Three jobs for one little reference.

Next I want to follow that reference beyond the parser. Can the application use database.host without taking over the database's lifetime? A plan showing which resources an update will change would give me something to inspect before applying it.

I'd like to try that with an application using a separate database. Change three replicas to four, read the plan, and see that the database is staying put. If I can make that easy to follow, this would give me a useful reason to keep working beyond the lexer. I am quite enjoying the lexer, though.

This entry came over from blog.haddon.me. I’ve tidied up the grammar and flow, bringing it closer to how I write now. I used AI to help with the grammar, structure and flow. I have dyslexia, and my writing often comes out as a rambling scrawl, so it can be hard to see what I was actually working on at the time. I think the tidied versions are easier to digest and more interesting to read. The artefacts got the same tidy-up while I was at it.

The artefact shown here was recreated in October 2026 and migrated to Piko's PKC system.