257 lines
6.1 KiB
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
|