Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1,043 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

k1 is a next-generation programming language with

  • typeclasses
  • full compile-time execution
  • programmable, stuctural types and rich type expressions (like Typescript)
  • modern generics, including typeclass bounds and compile-time parameters (fixlist[char, 64])
  • advanced sum and enum types
  • transparent and guaranteed layout and ABI (like C)
  • pattern matching
  • semi-automatic memory management
  • opt-in memory safety and garbage collection via Fil-C
  • capturing lambdas
  • arbitrarily powerful metaprogramming

Getting Started

To use k1, it is recommended to download the release bundle for your platform from releases and run install.sh after unzipping, and after reading it of course! There is a neovim plugin as well as a VSCode extension located here in the repo inside tools/vscode-k1.

For now, the language server support really helps bridge the complete lack of documentation, just let your editor teach you the language!

Tenets

Give the programmer power and control

k1 is designed around what it makes possible, not what it prevents, with a focus on freedom, control, and speed, while also providing advanced features, rather than punting on them, to empower programmers to make their programs exactly what they want them to be, not what the language wants them to be.

Compile fast

Shortening the feedback cycle preserves flow and greatly increases joy, as well as literally temporally enabling more iterations. This results in better software that is fun to work on. There are a number of language design decisions in k1 that were explicitly made to enable and preserve very fast compilation, like requiring function signatures to fully specify their types

Your programs should be as fast as your computer is

A career spent programming for the JVM has made me loathe inescapable costs. There are many fantastic features and idioms that can be free, such as newtypes, lambdas, even something as simple as the almighty struct, which many languages lack in favor of a boxed class as the primary unit of computation. This is a huge mistake; it forces the programmer to choose between something typesafe and idiomatic with overhead, or something fast. You end up doing metaprogramming heroics to implement a zero-cost option in Scala, or managing your own memory in Java.

Metaprogramming should be like programming

Compile-time execution and reflection enables powerful metaprogramming that is just regular programming.

As programmers, we've become exceedingly good at expressing and solving our problems using programming. Why can't we also solve our programming, and build, problems this way?

Plain old data everywhere, zero-is-initialized wherever possible, no RAII

De-coupling data from behavior allows for huge efficiency gains, free serialization, simple and obvious program structure, discourages convoluted structure.

Typeclasses, not inheritance

Just so good

No forced abstractions or costs

Pay for what you use, and use what you want. And what you use costs as little as possible. This is exactly Rust's philosophy and definition of 'zero-cost' functionality. I think it holds up and is a fantastic north star.

Contributing setup

To build from source, you'll want to install Rust, just, and LLVM 21. Rust may be installed via rustup. At the time of this writing, rustc 1.95 is the latest version and works great. You can install just via your package manager, pre-built binary, etc. LLVM may also be installed via your package manager. On Mac for example, brew install llvm@21. llvm-sys won't know where the install is at on its own though, so tell it via an environment variable in your shell config. Again on Mac for example:

export LLVM_SYS_211_PREFIX="$(brew --prefix llvm@21)"

If you're really serious about building binaries, there is a script located in the repo llvm/get_llvm.sh that will help you cut an LLVM 21 build from source, with the flags k1 needs to enable fully static linking of llvm.

With the dependencies installed, k1 and the language server may be built via:

just build-r
just lsprelease

The resulting binaries will be available under ./target/release. You can install the binaries and the dependent k1lib to ~/.k1 via:

just install-macos-from-macos # Or: just install-linux-from-linux

As the install command suggests, you'll want to add ~/.k1/bin to your PATH.

To smoke test your installation:

# Check out the available commands
k1
# Run a simple program
k1 run <(echo 'fn main(): i32 { println("hello k1!"); 0 }')

Inspiration

The programmer, like the poet, works only slightly removed from pure thought-stuff. He builds his castles in the air, from air, creating by exertion of the imagination. Few media of creation are so flexible, so easy to polish and rework, so readily capable of realizing grand conceptual structures.... Yet the program construct, unlike the poet's words, is real in the sense that it moves and works, producing visible outputs separate from the construct itself.

  • Brooks

The way to fall asleep is by pretending to be someone who is asleep. And that's how everything works

  • Someone on twitter

You can just do things

  • Popular memes archive, 2024

Case Study: implementing bitfields

Let's add a feature to k1 using metaprogramming to support bitfields. Video Version (the video predates some syntax changes; the code below is the current implementation, live at k1lib/std/bitfield.k1)

I'd like to be able to define a bitfield by providing a name, a 'base' integer type big enough to house all the fields I provide, and a series of fields as a collection of (name, bit width) pairs.

We can encode this in K1 easily enough as a function:

fn define[base](type-name: string, members: span[{ name: string, bits: size }]): ???

We use a type parameter, base, denoted in square brackets, to track our 'base type', which should be an unsigned integer type, which we can enforce later. k1 provides the usual friends: u8, u16, u32, and u64.

We also accept a name to attach to whatever constructs we end up generating for our flags. Perhaps functions to encode and decode each bit-field, and maybe some constants? Not sure yet.

Most importantly we accept a span of structs, with name and bits, telling us which 'bit' 'fields' we'd like to encode or pack into the value.

That leaves the return type, which we've left as ???. Ultimately we'd like to produce some code; most text-based programming languages are very well-represented and maintained in a particularly powerful format: source code text. So we'll return a string, (not a org.sys.meta.macros.whitebox.TTreeSyn.Quoted) containing the K1 code we'd otherwise have hand-written for our bitfield.

We'll want a few helpers. int-width-for-bits just crashes on bad input rather than returning a result[t, e], since we intend to run this code at compile-time:

fn int-width-for-bits(bits: size): size {
  if bits <= 8 8
  else if bits <= 16 16
  else if bits <= 32 32
  else if bits <= 64 64
  else crash("Too many bits: {bits}")
}
// Some simple constructors to make our metaprogram nice to invoke later
fn bn(name: string, bits: size): { name: string, bits: size } { { name, bits } }
fn b1(name: string): { name: string, bits: size } { bn(name = name, bits = 1) }
fn b8(name: string): { name: string, bits: size } { bn(name = name, bits = 8) }

Here comes a big wall of advanced metaprogramming code; but it may look strikingly like dumb string-building code to you.

fn define[base](type-name: string, members: span[{ name: string, bits: size }]): string {
  use meta/code-writer

  let base-type-id: u64 = types/type-id[base]()
  let base-schema = types/type-id-schema(base-type-id)
  let base-name = types/type-id-name(base-type-id)
  require base-schema is :int(int-kind) else { crash("Base should be an int; got {base-name}") }
  let total-bits = int-kind.bit-width()

  // This is just a regular string-builder that gets some extra methods since we've
  // brought the meta/code-writer ability into scope
  let code = string-builder/new()

  // This defines a named struct type with one field, which is a common newtype pattern
  writelnf(code, "type {type-name} = \{ bits: {base-name} }")

  // We crack open a namespace to put some goodies inside
  writelnf(code, "ns for {type-name} \{")

  // This actually gets captured by the lambda below
  let bit-index-val = 0
  let bit-index: *mut size = &bit-index-val

  // Let's analyze each member we were passed, and perform the actual bitfield layouting work once.
  // We'll make a list of an unnamed (anonymous) little struct type for our info we'll need later
  let member-info: list[{ 
    // mask: the positioned mask
    mask: usize,
    // type-width: the smallest type that can hold these bits; see `int-width-for-bits`
    type-width: size,
    // inv-mask: the negation of the mask, but also masked down to type-width size (to generate constants that fit)
    inv-mask: usize,
    // raw-value: the mask shifted to the start, the 'magnitude' of the used bits
    raw-value: usize,
    // offset: the bit pos that the mask starts at; where the value lives; how much to shift
    offset: size
  }] = list/from-mapping(members, fn member. {
    let bits = member.bits
    if bit-index.* + bits > total-bits crash("Too big: {bit-index.* + bits}")
    let raw-value = u64/bitmask-low(bits)
    let type-width = int-width-for-bits(bits)
    let mask = raw-value << bit-index.*.as()
    let offset = bit-index.*
    let size-mask = u64/bitmask-low(type-width)
    let inv-mask = mask.bit-not() & size-mask

    bit-index <- bit-index.* + bits

    { mask, type-width, inv-mask, raw-value, offset }
  })

  // The for loop works on any type that implements iterable or iterator
  // The item being iterated over is named `it` if no binding is supplied
  // `it-index` is also given to us
  for members {
    let info = member-info.get(it-index)
    // Emit globals, or constants, for each field's mask
    writelnf(code, "  let {it.name}-mask: {base-name} = {info.mask}")
  }

  writelnf(code, "  let zero: {type-name} = \{ bits: 0 }")

  // Now let's do a getter and setter for each bitfield. If the width is 1,
  // we'll use bool since that's what the people likely want. Note that we could configure
  // any of this behavior because this is just a regular function
  for members {
    let info = member-info.get(it-index)
    let is-bool = it.bits == 1
    let field-type = if is-bool "bool" else "u{info.type-width}"

    // Getter
    writelnf(code, "  fn get-{it.name}(self: {type-name}): {field-type} \{")

    writelnf(code, "    let bits: {base-name} = self.bits;")
    writelnf(code, "    let shifted: {base-name} = bits >> {info.offset};")
    writelnf(code, "    let cleared: {base-name} = shifted & {info.raw-value};")
    if is-bool {
      writelnf(code, "    cleared == 1")
    } else {
      writelnf(code, "    cleared.as[{field-type}]")
    }

    writelnf(code, "  }") // End Getter

    // Setter
    writelnf(code, "  fn set-{it.name}(self: {type-name}, value: {field-type}): {type-name} \{")
    if is-bool {
      writelnf(code, "    let masked-value: {base-name} = value.as-u8();")
    } else {
      writelnf(code, "    let masked-value: {base-name} = value & {info.raw-value};")
    }
    let clear-mask = info.inv-mask
    writelnf(code, "    let cleared-bits: {base-name} = self.bits & {clear-mask};")
    writelnf(code, "    let inserted-bits: {base-name} = cleared-bits | (masked-value << {info.offset});")
    writelnf(code, "    \{ bits: inserted-bits }")
    writelnf(code, "  }") // End setter
  }

  writelnf(code, "}") // End ns

  let print-body = string-builder/new()
  for members {
    let get-name = "get-{it.name}"
    let last = it-index + 1 == members.len()
    let sep = if last "" else ","
    writelnf(print-body, `    w.string("{it.name}=\{self.{get-name}()}{sep}");`)
  }
  (&code).impl-print(type-name, print-body.build(), indent = 0)

  code.build()
}

For an example input, let's encode some data about a starship into 16 bits.

  • bit 1: shielded
  • bit 2: cloaked
  • bit 3: damaged
  • bits 4-8: a sector id
  • bits 9-16: a byte named foo because what example is complete without a foo

We could test this function by just running it and inspecting the string, since it is a normal function, or we could ask k1 to run it for us and insert the result into our program using the #meta metaprogramming operator:

#meta std/bitfield/define[u16]("starship-flags",
  [b1("shielded"), b1("cloaked"), b1("damaged"), { name: "sector-id", bits: 5 }, bn("foo", 8)]
)

And now we can use the type and namespace starship-flags to pack some bits!

fn test-bitfield() {
  let y: starship-flags = { bits: 0b1111_0000_1000_1101 }
  //                                foo......|secto||||
  //                                                |||- shielded
  //                                                ||- cloaked
  //                                                |- damaged
  let x: starship-flags = starship-flags/zero.set-shielded(true).set-damaged(true).set-sector-id(17).set-foo(0b1111_0000)
  assert-equals(y.get-shielded(), x.get-shielded())
  assert-equals(y.get-cloaked(), x.get-cloaked())
  assert-equals(y.get-damaged(), x.get-damaged())
  assert-equals(y.get-sector-id(), x.get-sector-id())
}

Releases

Used by

Contributors

Languages