nipbox/kdl.nim

257 lines
6.1 KiB
Nim

# SPDX-License-Identifier: LUL-1.0
# Copyright (c) 2026 Markus Maiwald
# Stewardship: Self Sovereign Society Foundation
#
# This file is part of the Nexus SDK.
# See legal/LICENSE_UNBOUND.md for license terms.
# MARKUS MAIWALD (ARCHITECT) | VOXIS FORGE (AI)
# NipBox KDL Core (The Semantic Spine)
# Defines the typed object system for the Sovereign Shell.
import strutils
import std/assertions
type
ValueKind* = enum
VString, VInt, VBool, VNull
Value* = object
case kind*: ValueKind
of VString: s*: string
of VInt: i*: int
of VBool: b*: bool
of VNull: discard
# A KDL Node: name arg1 arg2 key=val { children }
Node* = ref object
name*: string
args*: seq[Value]
props*: seq[tuple[key: string, val: Value]]
children*: seq[Node]
# --- Constructors ---
proc newVal*(s: string): Value = Value(kind: VString, s: s)
proc newVal*(i: int): Value = Value(kind: VInt, i: i)
proc newVal*(b: bool): Value = Value(kind: VBool, b: b)
proc newNull*(): Value = Value(kind: VNull)
proc newNode*(name: string): Node =
new(result)
result.name = name
result.args = @[]
result.props = @[]
result.children = @[]
proc addArg*(n: Node, v: Value) =
n.args.add(v)
proc addProp*(n: Node, key: string, v: Value) =
n.props.add((key, v))
proc addChild*(n: Node, child: Node) =
n.children.add(child)
# --- Serialization (The Renderer) ---
proc `$`*(v: Value): string =
case v.kind
of VString: "\"" & v.s & "\"" # TODO: Escape quotes properly
of VInt: $v.i
of VBool: $v.b
of VNull: "null"
proc render*(n: Node, indent: int = 0): string =
let prefix = repeat(' ', indent)
var line = prefix & n.name
# Args
for arg in n.args:
line.add(" " & $arg)
# Props
for prop in n.props:
line.add(" " & prop.key & "=" & $prop.val)
# Children
if n.children.len > 0:
line.add(" {\n")
for child in n.children:
line.add(render(child, indent + 2))
line.add(prefix & "}\n")
else:
line.add("\n")
return line
# Table View (For Flat Lists)
proc renderTable*(nodes: seq[Node]): string =
var s = ""
for n in nodes:
s.add(render(n))
return s
# --- Parser ---
type Parser = ref object
input: string
pos: int
proc peek(p: Parser): char =
if p.pos >= p.input.len: return '\0'
return p.input[p.pos]
proc next(p: Parser): char =
if p.pos >= p.input.len: return '\0'
result = p.input[p.pos]
p.pos.inc
proc skipSpace(p: Parser) =
while true:
let c = p.peek()
if c == ' ' or c == '\t' or c == '\r': discard p.next()
else: break
proc parseIdentifier(p: Parser): string =
# Simple identifier: strictly alphanumeric + _ - for now
# TODO: Quoted identifiers
if p.peek() == '"':
discard p.next()
while true:
let c = p.next()
if c == '\0': break
if c == '"': break
result.add(c)
else:
while true:
let c = p.peek()
if c in {'a'..'z', 'A'..'Z', '0'..'9', '_', '-', '.', '/'}:
result.add(p.next())
else: break
proc parseValue(p: Parser): Value =
skipSpace(p)
let c = p.peek()
if c == '"':
# String
discard p.next()
var s = ""
while true:
let ch = p.next()
if ch == '\0': break
if ch == '"': break
s.add(ch)
return newVal(s)
elif c in {'0'..'9', '-'}:
# Number (Int only for now)
var s = ""
s.add(p.next())
while p.peek() in {'0'..'9'}:
s.add(p.next())
try:
return newVal(parseInt(s))
except:
return newVal(0)
elif c == 't': # true
if p.input.substr(p.pos, p.pos+3) == "true":
p.pos += 4
return newVal(true)
elif c == 'f': # false
if p.input.substr(p.pos, p.pos+4) == "false":
p.pos += 5
return newVal(false)
elif c == 'n': # null
if p.input.substr(p.pos, p.pos+3) == "null":
p.pos += 4
return newNull()
# Fallback: Bare string identifier
return newVal(parseIdentifier(p))
proc parseNode(p: Parser): Node =
skipSpace(p)
let name = parseIdentifier(p)
if name.len == 0: return nil
var node = newNode(name)
while true:
skipSpace(p)
let c = p.peek()
if c == '\n' or c == ';' or c == '}' or c == '\0': break
if c == '{': break # Children start
# Arg or Prop?
# Peek ahead to see if next is identifier=value
# Simple heuristic: parse identifier, if next char is '=', it's a prop.
let startPos = p.pos
let id = parseIdentifier(p)
if id.len > 0 and p.peek() == '=':
# Property
discard p.next() # skip =
let val = parseValue(p)
node.addProp(id, val)
else:
# Argument
# Backtrack? Or realize we parsed a value?
# If `id` was a bare string value, it works.
# If `id` was quoted string, `parseIdentifier` handled it.
# But `parseValue` handles numbers/bools too. `parseIdentifier` does NOT.
# Better approach:
# Reset pos
p.pos = startPos
# Check if identifier followed by =
# We need a proper lookahead for keys.
# For now, simplistic:
let val = parseValue(p)
# Check if we accidentally parsed a key?
# If val is string, and next char is '=', convert to key?
if val.kind == VString and p.peek() == '=':
discard p.next()
let realVal = parseValue(p)
node.addProp(val.s, realVal)
else:
node.addArg(val)
# Children
skipSpace(p)
if p.peek() == '{':
discard p.next() # skip {
while true:
skipSpace(p)
if p.peek() == '}':
discard p.next()
break
skipSpace(p)
# Skip newlines
while p.peek() == '\n': discard p.next()
if p.peek() == '}':
discard p.next()
break
let child = parseNode(p)
if child != nil:
node.addChild(child)
else:
# Check if just newline?
if p.peek() == '\n': discard p.next()
else: break # Error or empty
return node
proc parseKdl*(input: string): seq[Node] =
var p = Parser(input: input, pos: 0)
result = @[]
while true:
skipSpace(p)
while p.peek() == '\n' or p.peek() == ';': discard p.next()
if p.peek() == '\0': break
let node = parseNode(p)
if node != nil:
result.add(node)
else:
break