#badlang

2026-02-09

Ok I think I found a good name for my programming language. It's short and sweet and I haven't seen it used anywhere for other PL projects:

The Oni programming language

Named after the evil japanese yokai. Seems in-theme with the previous "badlang" name.

I've acquired oni-lang.com and will be updating my repos to use the new name soon. Perhaps this is a good opportunity to start adding a website and documentation.

#PLDev #badlang #onilang

2026-02-08

Yesterday I had the horrible realization that multiple people seems to have created programming languages called BadLang. To this end I think it’s probably time for me to choose a definitive name for my language. We floated the name of Badlands. But I need to ponder this a bit more. #pldev #badlang

2026-02-06

Made a little RMS/Peak meter that averages the measurement across N audio frames.

#badlang #pldev #PlayDate

2026-02-05

Hi there! Seems this little thing has a mic, neat!

#pldev #badlang #playdate

2026-02-05

Having a lot of fun with this, I sense a new toy project incoming :)

This is using the wavreader from the badlang standard library with a custom playdate IO.Reader and a simple sample playback DSP code. All written in my own little language.

#badlang #pldev #playdate

2026-02-04

Without too much trouble I managed to run Badlang code on the Playdate. Just had to override the panic function to use the pd system log and to create some wrapper functions.

I also added a custom allocator, meaning I can use most of the relevant stuff from the standard library. Here is shown a directory list, a file being read (and the written) and timing information. Additionally I can draw using the system drawing functions (for text and sprites) and directly on the framebuffer (as seen on the grid behind).

Perhaps I’ll have a bit more fun with this soon :)

#badlang #pldev

A playdate console running some demo software with some grid and text being drawn
2026-02-03

Started exploring Result::(Error, Val) types and implemented a `tryor` operator (??). The previous `try` operator (?) would unwrap or return the default zeroed value for the current function. With this new operator we can perform some extra action before returning and give our desired return type.

Equivalent to:

let a = funcall() ?? ret_expr
let a = {
let tmp = funcall()
if not tmp.ok() then return ret_expr
tmp.val()
}

Since `ret_expr` is an expression, you could log a message before returning or something like that.

Pretty happy with this approach, I don't like to just blindly propagate an error, it needs to get handled somewhere or transform across type boundaries, which this should be able to handle.

#badlang #pldev

Badlang code that reads a RIFF tag from an array iterator. It uses the ?? operator to return a result type on iterator read failure, leading to a small code footprint.
2026-02-02

Back home. Been doing some light programming on the side, namely bugfixing, adding support for typed enums (still only integer types, but this way you can stuff enums into structs and arrays and control their size) and exploring adding type aliases.

My initial implementation is super simple, just literally an alias so any function that takes the original type would also take the alias and vice-versa. Starting adding a more complex implementation where aliases are new types that could be casted back and forth between the original and alias implementation. This would become a hard typecheck failure if an alias is used instead of the original type and would enable having specific methods for aliased types.

Unsure if the complexity is worth the cost for this particular feature. Thoughts?

#badlang #pldev

2026-01-21

Following along Beej's networking guide [1] and implementing unix sockets bindings for Badlang. I thought it would be fun to make a simple HTTP server with this!

The C sockets API is not very ergonomic so I'm planing some improvements to it, but gotta play a bit more with it first.

[1]: beej.us/guide/bgnet/html/split

#badlang #pldev

Partial Badlang code showcasing an example for a simple HTTP server. To the right, the logging info screen shows: connecting to socket, preparing server, obtaining socket, binding socket, listening and accepting connections. A browser window renders the html output, a big Hello World.
2026-01-20

Added some basic multi-threading primitives wrapping over threads.h

#badlang #pldev

Showcase of some multi-threading code in Badlang using threads, mutexes and thread cond variables.
2026-01-19

Hello folks, I've updated the README of #badlang and would love to have some feedback (boost welcome).

Still need to work on the documentation and language reference but I'm trying to touch on the general summary of the project.

Let me know what you think!

git.badd10de.dev/badlang/about

#pldev

2026-01-19

I'm pretty happy with the current language at the moment, there may be a couple of extra small features I will consider adding before doing an official release. Most of the work now will go towards adding some stdlib stuff, and keep playing around with it, finding rough edges and having fun using it!

For now I've made it so #! is also a comment, that way I can use source files directly as scripts which I think it's pretty neat.

I've also added some initial tracing allocators to be able to log in allocations and de-allocations and added some basic logging functionality with a date and prefix tag.

#badlang #pldev

A badlang "script" that is run directly from a shell with ./example.bad. It reads as follows  (longish):

#!/usr/bin/env badc-run

import "std/debug"
import "std/allocators/arena"
import "std/allocators/tracing_arena"
import "std/allocators/tracing_allocator"
import "std/logger"

let buf: [1024]U8
let arena = TracingArena(Arena.init((@buf):Ptr, 1024), Dbg.fmt)
let alloc = TracingAllocator(arena.allocator(), Dbg.fmt).allocator()
let mem = alloc.malloc(32)
set mem = alloc.malloc(32)
alloc.free(mem, 32)
set mem = alloc.calloc(32)
set mem = alloc.calloc(32)
set mem = alloc.realloc(mem, 32, 128)
arena.reset()

let infolog = Logger(":: INFO ::", Dbg.fmt)
let errlog = Logger(":: ERROR ::", Dbg.fmt)
infolog.println("hello: % this is %")
    .str("ding")
    .int(32)
infolog.println("connecting to socket %").hex(1234)
errlog.println("something went wrong with error: %").int(42)
2026-01-18

I was thinking on embedding libtcc to be able to use TCC to run .bad files as if they were scripts.

But what if instead I just provide a simple bash script that serves the same function if installed alongside the badc and tcc executables? This keeps badc entirely dependency free other than libc, and that could also be removed. A fully static executable build it's already possible with musl, so it would be nice to be able to just provide a binary and the stdlib for releases.

#badlang #pldev

A bash file that compiles files with badc and tcc and a couple of examples of using it below it.

The file reads:
#!/bin/sh

file_name=$(mktemp --suffix ".c" --dry-run) || exit
trap 'rm -rf "$file_name"; exit' ERR EXIT

badc -o $file_name $@
tcc -std=c11 -O2 -run $file_name
2026-01-17

Back from my trip to BCN and back again to work on the compiler.

Did some code cleanup and finally added type inference for struct, union and choice type literals.

Naturally, type inference is not always possible in which case we can always use polytype annotations as before, but this is much more convenient for when it is.

#badlang #pldev

Badlang code with some before/after comparisons, where before we required explicit type annotations for polymorphic structs, unions and choice types and after there is type inference.

Reads as follow:

{
    struct A :: (T, Q) { x: T, y: Int, z: Q }
    let before = A::(U8, U16)(1:U8, 2, 3:U16)
    let after  = A(1:U8, 2, 3:U16)
}

{
    union A :: T { x: T, y: Int }
    let before = A::(Int)(x=1)
    let after  = A(x=1)
}

{
    choice A :: T { x: T, y: Int, z }
    let before = A::(F64).x(1.0)
    let after  = A.x(1.0)
}
2026-01-13

I added the following compiler directives:

#flag to set a compiler flag, like a #define in c but without value

#def set a compiler flag with a compile time string value.

#get gets a compile time flag value

This in combination with a —def parameter on the compiler enables me to finally have a compile time default path for the stdlib, so that I can do make install and the compiled binary will fetch the path of the system stdlib instead of the dev path.

I may expand this in the future for other constants, but this is sufficient for now. I would like to keep comptime stuff at a minimum.

#badlang #pldev

2026-01-10

Today I expanded on the direct to backend compiler directives:

#emit puts the given string directly on the source code as a LinearOp
#funattr adds function attributes to the current function
#global puts the string into the top of the generated file
#local puts the string into the top of the current function

With these in place you can do things like adding linear assembly (useful to insert optimization fences or other shenanigans), hookup instrumentation, and to configure your functions as you would with a C compiler (add always_inline, force loop unrolls, put a function into a given section, etc.). I think these form a base that could work for most or all backends I can think of, so they are not limited to the current C one.

#badlang #pldev

Badlang code showcasing some direct to backend code emission.
2026-01-10

Since yesterday I was looking at some tooling and it's integration with my language I started pondering if it would make sense to add some command line arguments to add instrumentation to functions, like so:

badc --inst-all-fun AAA
--inst-fun-with-prefix foo BBB
--inst-fun-with-suffix foo CCC
--inst-fun-matching foo DDD

This could be used to add timers or profiler hooks to the beginning of a function, and since we have `defer`, also to the end if we wanted to.

#pldev #badlang

2026-01-09

Now that I have a generic map implementation, I can finally use it to replace the previous Map::(Str, U32) with a Map::(Type, U32). This avoids the creation of unnecessary temporary strings that I was using for type uniqueness determination.

As a result, the semantic analysis now only uses a whooping 0.7MBs of temporary memory instead of 180MB that it was using previously. It's also about 14% faster but I was mainly looking for the reduction in memory usage.

#badlang #pldev

Some performance statistics:
Before
[SEMA]  PERM mem:  46.4793/2048.0000 MB
[SEMA]  TEMP mem:  187.2499/2048.0000 MB
[SEMA]  Time: 63.5395 ms
[SEMA]  KLOC/s: 232.4852

After
[SEMA]  PERM mem:  46.2802/2048.0000 MB
[SEMA]  TEMP mem:  0.7871/2048.0000 MB
[SEMA]  Time: 55.6295 ms
[SEMA]  KLOC/s: 265.5427
2026-01-09

Checking out this wonderful GDB frontend[1] by @nakst

Because I can emit line directives in debug mode I'm able to step through my code, set breakpoints and look and registers.

I wonder if there is something similar to #line but for registering locals or function names, otherwise my codegen locals look like _l0, _l1, _t0, etc. Still, pretty useful already!

[1] github.com/nakst/gf

#badlang #pldev

A debugger window with some badlang code on the source window, a gdb command line and some registers view.
2026-01-08

Taking some time to fix some bugs and do a pass of consistency and documentation over the current stdlib. Also started the implementation of a generic Map::(K, V) data structure.

In the current compiler I'm using specific implementations for IntMaps and StrMaps but they are just specialization with a lot of code shared. The new implementation also supports iterators that can be used on foreach loops.

#badlang #pldev

Badlang code showcasing a generic map from int to string, where 3 values are inserted, and then check if they exist in a for loop and by iterating over all the map elements with a foreach loop.

Client Info

Server: https://mastodon.social
Version: 2025.07
Repository: https://github.com/cyevgeniy/lmst