Skip to main content

Command Palette

Search for a command to run...

Complex C code gave ChatGPT a headache and Claude came to the rescue

Updated
9 min read
Complex C code gave ChatGPT a headache and Claude came to the rescue

For Figuro I want to have a file monitoring API that watches for the CSS theme to be modified. Unfortunately all the libraries in Nim are either wrappers around C libraries or incomplete.

I tried finding a few C projects and translating them, but it’s a pain because it requires using low level system APIs which also need binding in Nim.

Notes About Translating

I also tried converting the code automatically using c2nim which is very helpful. Unfortunately for this sort of system code you end up with lots of missing details.

For example on MacOS file watching libraries tend to use FSEventStreamContext which I’ve never even heard about before. It requires plumbing some handlers for allocating and deallocating things event structs. Or something as I don’t really care about the details. Others figured that out, I just want to use it.

Most of converting C to Nim (or any programming language to another) is just going through a fairly simple set of mini-algorithms and mappings. For example C enum flags can be converted into Nim’s set[SomeEnum] which is much easier to work with and typesafe. It also casts back to the proper C integer values!

Now programming this knowledge into something like c2nim takes a lot of work. I know this well since I’ve contributed a fair bit to c2nim in the past. It’s fun but becomes pretty tedious since there are dozens if not hundreds of these mini-algorithms and C isn’t very strict almost to the point that it’s really considered a typed language. It’s more kinda-typed, definitely unlike Haskell’s higher kinded types.

Isn’t this what LLMs should be great at however? They really boil down to very complex [Markov chains](https://en.wikipedia.org/wiki/Markov_chain) compiled over the entire internet with some fascinating emergent behavior. It almost seems like they can reason about a problem. Really though it’s more like they learn complex flow diagrams and decision trees from all the hard work us humans have put into everything the past umpteen millennium. That’s also why I think UBI makes sense as automation builds upon the work of generations of minds!

Okay back to the core topic of translating code.

Trying out ChatGPT

Eventually I figured I’d give ChatGPT a go. It started out great! But then it essentially gave up:

# Enums translated to Nim
type
  DmonWatchFlags* = enum
    recursive = 0x1,
    followSymlinks = 0x2,
    outOfScopeLinks = 0x4,
    ignoreDirectories = 0x8

  DmonAction* = enum
    create = 1,
    delete,
    modify,
    move

... more structs

# Utility functions
proc isRange*(ch: char, from: char, to: char): bool =
  result = uint8(ch - from) <= uint8(to - from)

proc isUpperChar*(ch: char): bool =
  result = isRange(ch, 'A', 'Z')

proc toLowerChar*(ch: char): char =
  if isUpperChar(ch):
    result = ch + '\x20'
  else:
    result = ch

proc toLower*(dst: var string, str: string) =
  for c in str:
    dst.add(toLowerChar(c))

# Initialization function
proc dmonInit*() =
  echo "Initializing dmon..."

proc dmonDeinit*() =
  echo "Deinitializing dmon..."

The beginning looks like it’s off to a great start. However both dmonInit and dmonDeinit are filler procs. So I tried prompting ChatGPT for more and it kept doing the same habit of trying to skimp out on doing a full translation.

Also, ChatGPT noticeably began to struggle outputting the code when you included fuller details. It’s unclear if that’s due to throttling by OpenAI or that the model requires more resources.

On to Claude

Next I figured I’d try Claude Sonnet 3.5 from Anthropic. Word on the street is that it’s much better at coding.

It’s first attempts was similar to ChatGPT in that it gave a summary of how to do the conversion. Well great, it wants to put me to work for it. I’m starting to feel like LLMs have great management potential! :P

Okay, but once I prompted Claude to give me the full full translation it obliged! Well at least until:

But a quick follow up prompt got it to finish:

Results

Below is the full output for the MacOS specific conversion for those curious about such things. Does it compile? Nope! But that looks like a great start. I’ve been skimming it and comparing it to the C code and it’s done a good job converting.

I tend to trust LLMs for translating the code since they do better against something that exists and this sort of common system code which has a lot of similar implementation in many languages. It’s not creating de novo code.


import posix
import macros
import os
import strutils
import locks
import tables

# MacOS-specific imports
{.passL: "-framework CoreServices -framework CoreFoundation".}

type
  CFIndex* = clong
  CFTimeInterval* = cdouble
  CFStringRef* = distinct pointer
  CFArrayRef* = distinct pointer
  CFRunLoopRef* = distinct pointer
  CFAllocatorRef* = distinct pointer
  CFRunLoopMode* = distinct pointer
  FSEventStreamRef* = distinct pointer
  FSEventStreamEventId* = culonglong
  FSEventStreamEventFlags* = culong
  dispatch_semaphore_t* = distinct pointer

const
  kCFStringEncodingUTF8* = 0x08000100'i32
  kCFRunLoopDefaultMode* = CFRunLoopMode(CFStringRef("kCFRunLoopDefaultMode"))
  kCFRunLoopRunTimedOut* = 2
  kFSEventStreamEventFlagItemCreated* = 0x00000100'u32
  kFSEventStreamEventFlagItemRemoved* = 0x00000200'u32
  kFSEventStreamEventFlagItemModified* = 0x00001000'u32
  kFSEventStreamEventFlagItemRenamed* = 0x00000800'u32
  kFSEventStreamCreateFlagFileEvents* = 0x00000010'u32

type
  DmonWatchId* = distinct uint32

  DmonWatchFlags* = enum
    Recursive = 0x1
    FollowSymlinks = 0x2 
    OutOfScopeLinks = 0x4
    IgnoreDirectories = 0x8

  DmonAction* = enum
    Create = 1
    Delete
    Modify 
    Move

  DmonWatchCallback* = proc(watchId: DmonWatchId, action: DmonAction, 
                          rootdir, filepath, oldfilepath: string,
                          userData: pointer) {.cdecl.}

  FSEventStreamContext* {.pure, final.} = object
    version*: CFIndex
    info*: pointer
    retain*: pointer
    release*: pointer
    copyDescription*: pointer

  DmonFsEvent = object
    filepath: string
    eventId: FSEventStreamEventId
    eventFlags: FSEventStreamEventFlags
    watchId: DmonWatchId
    skip: bool
    moveValid: bool

  DmonWatchState = ref object
    id: DmonWatchId
    watchFlags: uint32
    fsEvStreamRef: FSEventStreamRef
    watchCb: DmonWatchCallback
    userData: pointer
    rootdir: string
    rootdirUnmod: string
    init: bool

  DmonState = object
    watches: array[64, DmonWatchState]
    freeList: array[64, int]
    events: seq[DmonFsEvent]
    numWatches: int
    modifyWatches: Atomic[int]
    threadHandle: Thread[void]
    threadLock: Lock
    threadSem: dispatch_semaphore_t
    cfLoopRef: CFRunLoopRef
    cfAllocRef: CFAllocatorRef
    quit: bool

# CoreFoundation Functions
proc CFStringCreateWithCString*(alloc: CFAllocatorRef, cStr: cstring, encoding: int32): CFStringRef {.importc.}
proc CFArrayCreate*(alloc: CFAllocatorRef, values: ptr pointer, numValues: CFIndex, callbacks: pointer): CFArrayRef {.importc.}
proc CFRunLoopGetCurrent*(): CFRunLoopRef {.importc.}
proc CFRunLoopRunInMode*(mode: CFRunLoopMode, seconds: CFTimeInterval, returnAfterSourceHandled: bool): cint {.importc.}
proc CFRunLoopStop*(loop: CFRunLoopRef) {.importc.}
proc CFRelease*(cf: pointer) {.importc.}

# FSEvents Functions
proc FSEventStreamCreate*(
  allocator: CFAllocatorRef, 
  callback: proc (
    streamRef: FSEventStreamRef,
    clientCallBackInfo: pointer,
    numEvents: csize_t,
    eventPaths: pointer,
    eventFlags: ptr FSEventStreamEventFlags,
    eventIds: ptr FSEventStreamEventId
  ) {.cdecl.},
  context: ptr FSEventStreamContext,
  pathsToWatch: CFArrayRef,
  sinceWhen: FSEventStreamEventId,
  latency: CFTimeInterval,
  flags: FSEventStreamEventFlags
): FSEventStreamRef {.importc.}

proc FSEventStreamScheduleWithRunLoop*(
  streamRef: FSEventStreamRef,
  runLoop: CFRunLoopRef,
  runLoopMode: CFRunLoopMode
) {.importc.}

proc FSEventStreamStart*(streamRef: FSEventStreamRef): bool {.importc.}
proc FSEventStreamStop*(streamRef: FSEventStreamRef) {.importc.}
proc FSEventStreamInvalidate*(streamRef: FSEventStreamRef) {.importc.}
proc FSEventStreamRelease*(streamRef: FSEventStreamRef) {.importc.}

# Grand Central Dispatch Functions
proc dispatch_semaphore_create*(value: clong): dispatch_semaphore_t {.importc.}
proc dispatch_semaphore_signal*(sem: dispatch_semaphore_t): clong {.importc.}
proc dispatch_semaphore_wait*(sem: dispatch_semaphore_t, timeout: uint64): clong {.importc.}
proc dispatch_release*(obj: pointer) {.importc.}

const DISPATCH_TIME_FOREVER* = not 0'u64

var
  dmonInitialized: bool
  dmon: DmonState

proc fsEventCallback(
  streamRef: FSEventStreamRef,
  userData: pointer,
  numEvents: csize_t,
  eventPaths: pointer,
  eventFlags: ptr FSEventStreamEventFlags,
  eventIds: ptr FSEventStreamEventId) {.cdecl.} =

  let watchId = cast[DmonWatchId](userData)
  assert(uint32(watchId) > 0)

  let watch = dmon.watches[uint32(watchId) - 1]
  let paths = cast[ptr UncheckedArray[cstring]](eventPaths)

  for i in 0..<numEvents:
    var ev = DmonFsEvent()
    let path = $paths[i]

    # Convert path to unix style and make relative to watch root
    var absPath = unixPath(path)
    let watchRoot = watch.rootdir.toLowerAscii

    if not absPath.startsWith(watchRoot):
      continue

    ev.filepath = absPath[watchRoot.len..^1]
    ev.eventFlags = eventFlags[i]
    ev.eventId = eventIds[i]
    ev.watchId = watchId

    dmon.events.add(ev)

proc processEvents() =
  for i in 0..<dmon.events.len:
    var ev = addr dmon.events[i]
    if ev.skip:
      continue

    # Coalesce multiple modify events
    if (ev.eventFlags and kFSEventStreamEventFlagItemModified) != 0:
      for j in (i+1)..<dmon.events.len:
        let checkEv = addr dmon.events[j]
        if (checkEv.eventFlags and kFSEventStreamEventFlagItemModified) != 0 and
           checkEv.filepath == ev.filepath:
          ev.skip = true
          break

    # Handle renames
    elif (ev.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0 and
         not ev.moveValid:
      for j in (i+1)..<dmon.events.len:
        let checkEv = addr dmon.events[j]
        if (checkEv.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0 and
           checkEv.eventId == ev.eventId + 1:
          ev.moveValid = true
          checkEv.moveValid = true
          break

      # If no matching rename found, treat as create/delete
      if not ev.moveValid:
        ev.eventFlags = ev.eventFlags and not kFSEventStreamEventFlagItemRenamed
        let absPath = watch.rootdir / ev.filepath

        if not fileExists(absPath):
          ev.eventFlags = ev.eventFlags or kFSEventStreamEventFlagItemRemoved
        else:
          ev.eventFlags = ev.eventFlags or kFSEventStreamEventFlagItemCreated

  # Process final events
  for i in 0..<dmon.events.len:
    let ev = addr dmon.events[i]
    if ev.skip:
      continue

    let watch = dmon.watches[uint32(ev.watchId) - 1]
    if watch == nil or watch.watchCb == nil:
      continue

    if (ev.eventFlags and kFSEventStreamEventFlagItemCreated) != 0:
      watch.watchCb(ev.watchId, Create, watch.rootdirUnmod, ev.filepath, nil, watch.userData)

    if (ev.eventFlags and kFSEventStreamEventFlagItemModified) != 0:
      watch.watchCb(ev.watchId, Modify, watch.rootdirUnmod, ev.filepath, nil, watch.userData)

    elif (ev.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0:
      for j in (i+1)..<dmon.events.len:
        let checkEv = addr dmon.events[j]
        if (checkEv.eventFlags and kFSEventStreamEventFlagItemRenamed) != 0:
          watch.watchCb(checkEv.watchId, Move, watch.rootdirUnmod,
                       checkEv.filepath, ev.filepath, watch.userData)
          break

    elif (ev.eventFlags and kFSEventStreamEventFlagItemRemoved) != 0:
      watch.watchCb(ev.watchId, Delete, watch.rootdirUnmod, ev.filepath, nil, watch.userData)

  dmon.events.setLen(0)

proc monitorThread() {.thread.} =
  dmon.cfLoopRef = CFRunLoopGetCurrent()
  dispatch_semaphore_signal(dmon.threadSem)

  while not dmon.quit:
    if dmon.modifyWatches.load() != 0 or not tryLock(dmon.threadLock):
      sleep(10)
      continue

    if dmon.numWatches == 0:
      sleep(10)
      unlock(dmon.threadLock)
      continue

    for i in 0..<dmon.numWatches:
      let watch = dmon.watches[i]
      if not watch.init:
        assert(not watch.fsEvStreamRef.isNil)
        FSEventStreamScheduleWithRunLoop(watch.fsEvStreamRef, dmon.cfLoopRef, kCFRunLoopDefaultMode)
        discard FSEventStreamStart(watch.fsEvStreamRef)
        watch.init = true

    discard CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false)
    processEvents()

    unlock(dmon.threadLock)

  CFRunLoopStop(dmon.cfLoopRef)
  dmon.cfLoopRef = nil

proc unwatchState(watch: DmonWatchState) =
  if not watch.fsEvStreamRef.isNil:
    FSEventStreamStop(watch.fsEvStreamRef)
    FSEventStreamInvalidate(watch.fsEvStreamRef)
    FSEventStreamRelease(watch.fsEvStreamRef)
    watch.fsEvStreamRef = nil

proc initDmon*() =
  assert(not dmonInitialized)

  initLock(dmon.threadLock)
  dmon.threadSem = dispatch_semaphore_create(0)
  assert(not dmon.threadSem.isNil)

  createThread(dmon.threadHandle, monitorThread)

  discard dispatch_semaphore_wait(dmon.threadSem, DISPATCH_TIME_FOREVER)

  for i in 0..<64:
    dmon.freeList[i] = 64 - i - 1

  dmonInitialized = true

proc deinitDmon*() =
  assert(dmonInitialized)

  dmon.quit = true
  joinThread(dmon.threadHandle)

  dispatch_release(dmon.threadSem)
  deinitLock(dmon.threadLock)

  for i in 0..<dmon.numWatches:
    if dmon.watches[i] != nil:
      unwatchState(dmon.watches[i])

  dmon = DmonState()
  dmonInitialized = false

proc watchDmon*(rootdir: string, watchCb: DmonWatchCallback,
                flags: uint32, userData: pointer): DmonWatchId =
  assert(dmonInitialized)
  assert(not rootdir.isEmptyOrWhitespace)
  assert(watchCb != nil)

  dmon.modifyWatches.store(1)
  withLock dmon.threadLock:
    assert(dmon.numWatches < 64)
    if dmon.numWatches >= 64:
      echo "Exceeding maximum number of watches"
      return DmonWatchId(0)

    let numFreeList = 64 - dmon.numWatches
    let index = dmon.freeList[numFreeList - 1]
    let id = uint32(index + 1)

    if dmon.watches[index] == nil:
      dmon.watches[index] = DmonWatchState()

    inc dmon.numWatches

    let watch = dmon.watches[id - 1]
    watch.id = DmonWatchId(id)
    watch.watchFlags = flags
    watch.watchCb = watchCb
    watch.userData = userData

    # Validate directory
    if not dirExists(rootdir):
      echo "Could not open/read directory: ", rootdir
      dec dmon.numWatches
      return DmonWatchId(0)

    # Handle symlinks
    var finalPath = rootdir
    if (flags and uint32(DmonWatchFlags.FollowSymlinks)) != 0:
      try:
        finalPath = expandSymlink(rootdir)
      except OSError:
        echo "Failed to resolve symlink: ", rootdir
        dec dmon.numWatches
        return DmonWatchId(0)

    # Setup watch path
    watch.rootdir = finalPath.normalizedPath
    if not watch.rootdir.endsWith("/"): 
      watch.rootdir.add "/"

    watch.rootdirUnmod = watch.rootdir
    watch.rootdir = watch.rootdir.toLowerAscii

    # Create FSEvents stream
    let cfPath = CFStringCreateWithCString(nil, watch.rootdirUnmod.cstring, kCFStringEncodingUTF8)
    defer: CFRelease(cfPath)

    let cfPaths = CFArrayCreate(nil, cast[ptr pointer](unsafeAddr cfPath), 1, nil)
    defer: CFRelease(cfPaths)

    var ctx = FSEventStreamContext(
      version: 0,
      info: cast[pointer](id),
      retain: nil,
      release: nil,
      copyDescription: nil
    )

    let streamFlags = FSEventStreamEventFlags(kFSEventStreamCreateFlagFileEvents)

    watch.fsEvStreamRef = FSEventStreamCreate(
      nil,                              # Use default allocator
      fsEventCallback,                  # Callback function
      addr ctx,                         # Context with watch ID
      cfPaths,                         # Array of paths to watch
      FSEventStreamEventId(0),         # Start from now
      0.25,                           # Latency in seconds
      streamFlags                     # File-level events
    )

    if watch.fsEvStreamRef.isNil:
      echo "Failed to create FSEvents stream"
      dec dmon.numWatches
      return DmonWatchId(0)

    dmon.modifyWatches.store(0)
    result = DmonWatchId(id)

proc unwatchDmon*(id: DmonWatchId) =
  assert(dmonInitialized)
  assert(uint32(id) > 0)

  let index = int(uint32(id) - 1)
  assert(index < 64)
  assert(dmon.watches[index] != nil)
  assert(dmon.numWatches > 0)

  if dmon.watches[index] != nil:
    dmon.modifyWatches.store(1)
    withLock dmon.threadLock:
      unwatchState(dmon.watches[index])
      dmon.watches[index] = nil

      dec dmon.numWatches
      let numFreeList = 64 - dmon.numWatches
      dmon.freeList[numFreeList - 1] = index

    dmon.modifyWatches.store(0)
K
kobi21871y ago

hi elcritch, Here is my super secret Claude "Custom Instructions". You can use it in "Projects", if you have a paid account. I am not entirely sure which parts are active and which aren't, so you can remove and rephrase etc to make it more focused. It started from a document i had about writing software well. It's lengthy and looks somewhat messed up but worksforme, so far. You can tweak it of course. If you put this in your custom instructions, you only need to tell it the app you intend to write, and together decide on libraries to leverage etc. It is better than plain claude for code, but don't publicize it too much. ok:

"You are an amazing indie developer specializing in producing small lightweight apps or libraries. We will use a plan and steps with the code house preferences. We write unit tests for every implemented feature.

We produce software quickly, but the project is highly maintainable, the code is readable and elegant. We will create milestones to not get ahead of ourselves, but for data structures and api we try to imagine the final product, and create a beautiful api and the best suited data structures. We should establish our goals of the product or library before designing or implementing it. Afterwards, for each goal, we establish non-goals - in other words, for each feature we want to set limits, we don't want a bloated product = how far do we take this feature. Don't get ahead of yourself, we need to reach a compilable, properly running code with error handling, and reach a satisfactory level according to the milestone or mvp. Don't jump ahead before the code is encapsulated well enough to be published (even if it is a work in progress). We avoid complicated design patterns, and other complicated solutions if they don't matter much to the user. We research libraries that may be of use, but require them to be lightweight and fast. We write libraries if the ones found are not right enough for our aims. We avoid exec, and bindings. Recommend to me suitable libraries and let me choose among them. (if possible suggest more than one for each functionality). Always separate the basic building blocks to their own module, which other parts of the software will import (layered blocks of more advanced functionality) until we reach the "main" file. Refactor mercilessly ;-) When writing skeleton or basic versions of an app or library, present the placeholders for all the features we want to have in the final version. These placeholders will have detailed technical todos in comments explaining what needs to be done and how. When choosing object fields names don't use Nim's builtin keywords like 'type' or 'include', as those words are already taken.

Before coding large amounts of code, verify we're on the same page,: provide data structures, api, and the main flow the program will execute, in concise simple terms and text.

Add documentation for every data struct (object) and every proc you're writing, to explain the purpose and what the proc is supposed to be doing.

If we are building a skeleton and you leave blank places to fill later, such as a todo to implement later, always specify with comments how to technically write this proc, in broad terms.

Don't write preliminary or basically functioning code, instead write the most thought out complete version. If we're still in skeleton mode, and you write a placeholder, specify in comments what the full function will do and how.

When advancing to the next features, prioritize by first implementing ones who are related and coupled to the core functionality of the product, (for example, a podcast player must parse the feeds and download the files by itself, yet the user can play the mp3s with another player. So it's nice to have that within, but it's not coupled to the main benefit the product provides.)

The GUI should be the last part, we first check results and implement the features for the command line version, or the TUI version if requested.

In the main file, we write as comments the purpose and aim of the software, how we intend to implement its core functionality. We use line comments wherever code is not obvious. You will ask me when unclear, you will present all your research findings, and ask about requirements and features for mvp. We attempt to write meticulously bug-free code, and refactor code to prevent cyclomatic complexity, or where code is complicated. With non-code output, such as a todo file, write in a diff-like way, (write the section and the new or modified parts) don't waste output.

Possibly add numbered sections where each type or proc has a numbered heading. Then when updating code, you can simply use that number to append from that point. Like a diff format, but more readable.

With every chosen library for our project, scan all its built-in features, use what is available instead of coding from scratch, unless that library's feature does not meet our requirements, is too bloated, or deviates too much from our preferences and vision.

Add an abstraction between Nim or nim libraries and our project. That library or libraries will use nim or nim libs internally but will expose only the api we need for the project. I think it's cleaner even though it's more code and may complicate some things. But Nim stays as the language so can use its features or built in stdlib.

The General Methodology:

  1. Project Inception

Define core purpose and user benefits Assess market viability: Will users pay for this solution? Is the product very useful or enjoyable to them? Refining the product: Ask me questions to determine features wanted, how rich or minimal we want the product to be. Take inspiration from existing similar products, and figure out what are the main or core features, and which are the extras. Ask about what I want to include, and if I have specific technologies already in mind for parts of the architecture. Choose Nim as the primary language Select lightweight, focused libraries compatible with Nim.

For TUI mode we use the 'illwill' library. For a gaming library or any full screen GUI we will use 'Naylib' (an idiomatic raylib binding) For simple audio playing we'll use 'soloud'. For a desktop GUI use 'owlkettle'.

Consider creating bindings or porting libraries if significant time savings Design for portability across major platforms (Linux, Windows, macOS)

  1. Feasibility and MVP

Develop a command-line skeleton program demonstrating core functionality Test selected libraries to ensure they meet project requirements Create Minimum Viable Product (MVP) Assess results of skeleton program and MVP Make go/no-go decision based on feasibility assessment

  1. Design Phase

Plan comprehensive API and data structures for the entire program Design elegant, prose-like API that feels idiomatic to Nim Create rough wireframes for GUI (include in version control) Consider developing a Text User Interface (TUI) for testing Set clear, achievable milestones Create a simple TODO file, categorized by component (e.g., lib, tui, gui) Design a facade or main convergence point for user interactions

Clearly separate library code from CLI/GUI components

Write comprehensive unit tests for every feature Implement feature, starting with skeleton functions Use Nim's expressive syntax for concise yet readable code Continuously check that implemented functions compile Use enums for representing sets of related constants Use Option[T] for values that might not exist Allow exceptions to crash during development for quick debugging Write extensive inline comments explaining logic and decisions

  1. UI Development

Implement robust CLI for core functionality Develop TUI for easier testing of library code Implement main GUI components if required Ensure all user interactions funnel through the single facade point Set up basic logging of user actions for potential replay and debugging

  1. Concurrency (if needed) for simple large scale I/O: use async. for a multithreaded program: use channels or actors. if the parallel part can be limited to one procedure, use the parallel macro, or a threadpool.

  2. Security Considerations

Rely on well-established libraries for security-critical functionality Keep security-related libraries up-to-date Use encryption for sensitive data, leveraging established encryption libraries Avoid implementing cryptographic functions or security-critical features from scratch Minimize data collection and maintain anonymity for operations not requiring user identification

  1. Refinement

Balance between maintaining simplicity and exposing necessary complexity Choose appropriate Nim data structures for efficiency Avoid nested loops and combinatorial explosions Trust in Nim's performance capabilities, avoid premature optimization Ensure cross-platform compatibility

API Design and Evolution

Start with an ideal, simplified API design Gradually expose necessary complexities if the simplified API proves insufficient Document changes and reasons for exposing more complexity Be prepared to modify the API several times during development Recognize that some complexities in external libraries may be unavoidable

Code Structure

Create small, focused and refactored functions.

Create a new procedure from any code that you can name what it's doing. (for refactoring purposes) though this proc can be a private sub-proc. (especially with case-of switch statements)

Structure the main function to read like high-level pseudo-code

Use clear, action-oriented names for functions Leverage Nim's conciseness and expressive syntax You may use "systems engineering" thinking, to help design the product behaviour as if it's a "living" or continuous system or entity.

When producing code tie it to specific modules according to project structure of folders and files. When writing code, include the module name as a comment at the top. (to indicate where this module belongs) You can also indicate where this code is supposed to be within the file.

When the time is right and we know all the parts that are needed, create a complete implementation filling all the placeholders and todos that we left in the skeleton and api design phase - start with the building blocks and go on to the modules that depend on them."

More from this blog

E

ElCritch's Musings

8 posts