Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/swift/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
36 changes: 36 additions & 0 deletions packages/swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "OpenUI",
platforms: [
.macOS(.v13),
.iOS(.v16)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "OpenUILang",
targets: ["OpenUILang"]),
.library(
name: "OpenUISwiftUI",
targets: ["OpenUISwiftUI"]),
.executable(
name: "SwiftUIChat",
targets: ["SwiftUIChat"])
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "OpenUILang"),
.target(
name: "OpenUISwiftUI",
dependencies: ["OpenUILang"]),
.executableTarget(
name: "SwiftUIChat",
dependencies: ["OpenUILang", "OpenUISwiftUI"])
]
)
40 changes: 40 additions & 0 deletions packages/swift/Sources/OpenUILang/AST.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
public indirect enum ASTNode {
case comp(name: String, args: [ASTNode], mappedProps: [String: ASTNode]?)
case str(String)
case num(Double)
case bool(Bool)
case null
case arr([ASTNode])
case obj([(String, ASTNode)])
case ref(String)
case ph(String)
case stateRef(String)
case runtimeRef(name: String, refType: String)
case binOp(op: String, left: ASTNode, right: ASTNode)
case unaryOp(op: String, operand: ASTNode)
case ternary(cond: ASTNode, then: ASTNode, `else`: ASTNode)
case member(obj: ASTNode, field: String)
case index(obj: ASTNode, index: ASTNode)
case assign(target: String, value: ASTNode)

public var isRuntimeExpr: Bool {
switch self {
case .stateRef, .runtimeRef, .binOp, .unaryOp, .ternary, .member, .index, .assign:
return true
default:
return false
}
}
}

public struct CallNode {
public let callee: String
public let args: [ASTNode]
}

public enum Statement {
case value(id: String, expr: ASTNode)
case state(id: String, initExpr: ASTNode)
case query(id: String, call: CallNode, expr: ASTNode, deps: [String]?)
case mutation(id: String, call: CallNode, expr: ASTNode)
}
33 changes: 33 additions & 0 deletions packages/swift/Sources/OpenUILang/Library.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

public struct ComponentDef {
public let name: String
public let description: String
public let signature: String

public init(name: String, description: String, signature: String) {
self.name = name
self.description = description
self.signature = signature
}
}

public protocol AnyComponentRenderer {
// In OpenUISwiftUI we will cast this to AnyView
func render(props: [String: Any], children: [Any]) -> Any
}

public class ComponentLibrary {
public private(set) var components: [String: ComponentDef] = [:]
public var root: String?

public init() {}

public func register(component: ComponentDef) {
components[component.name] = component
}

public func setRoot(_ root: String) {
self.root = root
}
}
197 changes: 197 additions & 0 deletions packages/swift/Sources/OpenUILang/Parser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import Foundation

public class Parser {
private var input: String
private var index: String.Index

public init() {
self.input = ""
self.index = "".startIndex
}

public func parse(_ text: String) -> ParseResult {
self.input = text
self.index = text.startIndex

var statements: [Statement] = []

while index < input.endIndex {
skipWhitespace()
guard index < input.endIndex else { break }

if let id = parseIdentifier() {
skipWhitespace()
if match("=") {
skipWhitespace()
if let expr = parseExpression() {
statements.append(.value(id: id, expr: expr))
}
}
} else {
// Skip unparseable tokens
index = input.index(after: index)
}
}

var rootNode: ElementNode? = nil
if let rootStmt = statements.first(where: {
if case let .value(id, _) = $0 { return id == "root" }
return false
}) {
if case let .value(_, expr) = rootStmt {
rootNode = materialize(expr)
}
}

let meta = ParseResultMeta(incomplete: false, unresolved: [], orphaned: [], statementCount: statements.count, errors: [])
return ParseResult(root: rootNode, meta: meta, stateDeclarations: [:], queryStatements: [], mutationStatements: [])
}

private func parseExpression() -> ASTNode? {
skipWhitespace()
if index >= input.endIndex { return nil }

let c = input[index]
if c == "\"" {
return .str(parseString())
} else if c == "[" {
return .arr(parseArray())
} else if c.isNumber {
return .num(parseNumber())
} else if input[index...].hasPrefix("true") {
advance(by: 4); return .bool(true)
} else if input[index...].hasPrefix("false") {
advance(by: 5); return .bool(false)
} else if let ident = parseIdentifier() {
skipWhitespace()
if match("(") {
let args = parseArguments()
return .comp(name: ident, args: args.0, mappedProps: args.1)
} else {
return .ref(ident)
}
}
return nil
}

private func parseString() -> String {
advance() // skip "
var result = ""
while index < input.endIndex && input[index] != "\"" {
result.append(input[index])
advance()
}
if index < input.endIndex { advance() } // skip "
return result
}

private func parseNumber() -> Double {
var result = ""
while index < input.endIndex && (input[index].isNumber || input[index] == ".") {
result.append(input[index])
advance()
}
return Double(result) ?? 0
}

private func parseArray() -> [ASTNode] {
advance() // skip [
var elements: [ASTNode] = []
while index < input.endIndex {
skipWhitespace()
if match("]") { break }
if let expr = parseExpression() {
elements.append(expr)
}
skipWhitespace()
_ = match(",")
}
return elements
}

private func parseArguments() -> ([ASTNode], [String: ASTNode]) {
var args: [ASTNode] = []
var props: [String: ASTNode] = [:]

while index < input.endIndex {
skipWhitespace()
if match(")") { break }

// Try to parse named arg: ident: expr
let startIdx = index
if let ident = parseIdentifier() {
skipWhitespace()
if match(":") {
if let expr = parseExpression() {
props[ident] = expr
}
} else {
// It was just an expression
index = startIdx
if let expr = parseExpression() {
args.append(expr)
}
}
} else {
if let expr = parseExpression() {
args.append(expr)
}
}

skipWhitespace()
_ = match(",")
}

return (args, props)
}

private func parseIdentifier() -> String? {
guard index < input.endIndex, input[index].isLetter || input[index] == "_" else { return nil }
var result = ""
while index < input.endIndex && (input[index].isLetter || input[index].isNumber || input[index] == "_") {
result.append(input[index])
advance()
}
return result
}

private func skipWhitespace() {
while index < input.endIndex && input[index].isWhitespace {
advance()
}
}

private func match(_ str: String) -> Bool {
if input[index...].hasPrefix(str) {
advance(by: str.count)
return true
}
return false
}

private func advance(by count: Int = 1) {
index = input.index(index, offsetBy: count, limitedBy: input.endIndex) ?? input.endIndex
}

private func materialize(_ node: ASTNode) -> ElementNode? {
if case let .comp(name, _, mappedProps) = node {
var propsMap: [String: Any] = [:]
if let mProps = mappedProps {
for (k, v) in mProps {
if case let .str(s) = v { propsMap[k] = s }
else if case let .num(n) = v { propsMap[k] = n }
else if case let .bool(b) = v { propsMap[k] = b }
else if case let .arr(arr) = v {
propsMap[k] = arr.compactMap { materialize($0) }
} else if case .comp = v {
if let child = materialize(v) {
propsMap[k] = [child] // simplified
}
}
}
}
return ElementNode(statementId: nil, typeName: name, props: propsMap, partial: false)
}
return nil
}
}
47 changes: 47 additions & 0 deletions packages/swift/Sources/OpenUILang/PromptGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Foundation

public struct PromptOptions {
public var preamble: String?
public var additionalRules: [String]?
public var examples: [String]?
public var toolExamples: [String]?
public var tools: [String]?

public init(preamble: String? = nil, additionalRules: [String]? = nil, examples: [String]? = nil, toolExamples: [String]? = nil, tools: [String]? = nil) {
self.preamble = preamble
self.additionalRules = additionalRules
self.examples = examples
self.toolExamples = toolExamples
self.tools = tools
}
}

public class PromptGenerator {
public static func generatePrompt(library: ComponentLibrary, options: PromptOptions? = nil) -> String {
var prompt = ""

if let preamble = options?.preamble {
prompt += preamble + "\n\n"
} else {
prompt += "You are a UI generation assistant. Respond ONLY with valid OpenUI Lang code.\n\n"
}

prompt += "### Components\n\n"
for (_, comp) in library.components.sorted(by: { $0.key < $1.key }) {
prompt += "- \(comp.signature): \(comp.description)\n"
}

if let root = library.root {
prompt += "\nRoot Component: \(root)\n"
}

if let rules = options?.additionalRules, !rules.isEmpty {
prompt += "\n### Rules\n"
for rule in rules {
prompt += "- \(rule)\n"
}
}

return prompt.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
Loading
Loading