LanguageSpecs.kt
src/jvmMain/kotlin/afield/runtime/languages/LanguageSpecs.kt · 308 lines
package afield.runtime.languages
The text conventions of the languages a box property can hold: each LanguageSpec says how to wrap a property's source before it is compiled and how to unwrap it again.
Kotlin box code is wrapped by wrapScript into a fun SCRIPT() block between preamble and
postamble markers, keeping leading import and @file lines above it, and unwrapScriptLenient
takes it back out. GLSL specs normalise the preamble to #version 450 followed by the source's
#extension lines; the ray-tracing stages use #version 460 and always add GL_EXT_ray_tracing.
Slang specs only trim trailing whitespace.
glslSpecForProperty and shaderSpecForProperty map a property name to its spec, case-insensitively:
vertex, fragment, mesh, task, compute and the ray-tracing stages (raygen, miss,
closesthit, anyhit, intersection, callable) are GLSL, and the same names with an _s suffix,
plus vertex_fragment, are Slang. NANO_ASM is the spec for the Nano assembly language.
data class LanguageSpec
A language's name and the pair of functions that wrap its source for compiling and unwrap it again.
data class LanguageSpec(
val name: String,
val wrap: (String) -> String,
val unwrap: (String) -> String
)
class MalformedAI(message: String) : RuntimeException(message)
Thrown when a model's output still carries a reply marker such as <help> or <cursor>, so it is refused rather than applied.
private fun normalizeGlslPreamble(code: String): String {
val lines = code.trimEnd().split("\n")
val extensions = mutableListOf<String>()
val body = mutableListOf<String>()
for (line in lines) {
val trimmed = line.trim()
when {
trimmed.startsWith("#version") -> continue // normalize to required Vulkan GLSL version
trimmed.startsWith("#extension") -> {
extensions += trimmed
continue
}
else -> body += line
}
}
return buildString {
append("#version 450")
if (extensions.isNotEmpty()) {
append("\n")
append(extensions.joinToString("\n"))
}
if (body.isNotEmpty()) {
append("\n\n")
append(body.joinToString("\n"))
}
}.trimEnd()
}
private fun normalizeSlangSource(code: String): String = code.trimEnd()
fun wrapScript(script: String): String
Wraps top-level Kotlin script into a fun SCRIPT() block with preamble/postamble markers. Keeps leading import/@file lines at top.
fun wrapScript(script: String): String {
val lines = script.split("\n")
val importLines = lines.takeWhile { it.trim().startsWith("import ") || it.trim().startsWith("@file") }
val bodyLines = lines.drop(importLines.size)
return buildString {
if (importLines.isNotEmpty()) {
append(importLines.joinToString("\n"))
append("\n\n")
}
append("/*preamble*/ fun SCRIPT() {\n")
append(bodyLines.joinToString("\n"))
if (bodyLines.isNotEmpty()) append("\n")
append("/*postamble*/ }")
}
}
fun unwrapScriptLenient(wrapped: String): String
Lenient unwrap for Kotlin script output.
body — 51 lines, from line 84
fun unwrapScriptLenient(wrapped: String): String {
val lines = wrapped.split("\n")
// Case A: markers exist
val preambleIdx = lines.indexOfFirst { it.contains("/*preamble*/") }
val postambleIdx = lines.indexOfFirst { it.contains("/*postamble*/") }
if (preambleIdx != -1 && postambleIdx != -1 && postambleIdx > preambleIdx) {
val importLines = lines.subList(0, preambleIdx).dropLastWhile { it.isBlank() }
val bodyLines = lines.subList(preambleIdx + 1, postambleIdx)
return buildString {
if (importLines.isNotEmpty()) {
append(importLines.joinToString("\n"))
if (bodyLines.isNotEmpty()) append("\n\n")
}
append(bodyLines.joinToString("\n"))
}.trimEnd()
}
// Case B: salvage SCRIPT() without markers
val scriptStart = lines.indexOfFirst { it.contains("fun SCRIPT()") && it.contains("{") }
if (scriptStart != -1) {
var depth = 0
var started = false
val body = mutableListOf<String>()
for (i in scriptStart until lines.size) {
val line = lines[i]
val opens = line.count { it == '{' }
val closes = line.count { it == '}' }
if (!started) {
depth += opens - closes
started = true
continue
} else {
depth += opens - closes
if (depth <= 0) break
body += line
}
}
val imports = lines.take(scriptStart).dropLastWhile { it.isBlank() }
return buildString {
if (imports.isNotEmpty()) {
append(imports.joinToString("\n"))
if (body.isNotEmpty()) append("\n\n")
}
append(body.joinToString("\n"))
}.trimEnd()
}
// Case C: already a top-level script
return wrapped.trimEnd()
}
val KOTLIN_SCRIPT: LanguageSpec
val KOTLIN_SCRIPT: LanguageSpec = LanguageSpec(
name = "Kotlin",
wrap = ::wrapScript,
unwrap = ::unwrapScriptLenient
)
val GLSL_VERTEX: LanguageSpec
val GLSL_VERTEX: LanguageSpec = LanguageSpec(
name = "GLSL (vertex)",
wrap = ::normalizeGlslPreamble,
unwrap = ::normalizeGlslPreamble
)
val GLSL_FRAGMENT: LanguageSpec
val GLSL_FRAGMENT: LanguageSpec = LanguageSpec(
name = "GLSL (fragment)",
wrap = ::normalizeGlslPreamble,
unwrap = ::normalizeGlslPreamble
)
val GLSL_MESH: LanguageSpec
val GLSL_MESH: LanguageSpec = LanguageSpec(
name = "GLSL (mesh)",
wrap = ::normalizeGlslPreamble,
unwrap = ::normalizeGlslPreamble
)
val GLSL_TASK: LanguageSpec
val GLSL_TASK: LanguageSpec = LanguageSpec(
name = "GLSL (task)",
wrap = ::normalizeGlslPreamble,
unwrap = ::normalizeGlslPreamble
)
val GLSL_COMPUTE: LanguageSpec
val GLSL_COMPUTE: LanguageSpec = LanguageSpec(
name = "GLSL (compute)",
wrap = ::normalizeGlslPreamble,
unwrap = ::normalizeGlslPreamble
)
private fun normalizeRayTracingGlslPreamble(code: String): String {
val lines = code.trimEnd().split("\n")
val extensions = linkedSetOf("#extension GL_EXT_ray_tracing : require")
val body = mutableListOf<String>()
for (line in lines) {
val trimmed = line.trim()
when {
trimmed.startsWith("#version") -> continue
trimmed.startsWith("#extension") -> {
extensions += trimmed
continue
}
else -> body += line
}
}
return buildString {
append("#version 460")
append("\n")
append(extensions.joinToString("\n"))
if (body.isNotEmpty()) {
append("\n\n")
append(body.joinToString("\n"))
}
}
}
val GLSL_RAY_TRACING: LanguageSpec
val GLSL_RAY_TRACING: LanguageSpec = LanguageSpec(
name = "GLSL (ray generation)",
wrap = ::normalizeRayTracingGlslPreamble,
unwrap = ::normalizeRayTracingGlslPreamble
)
val GLSL_MISS: LanguageSpec = GLSL_RAY_TRACING.copy(name = "GLSL (miss)")
val GLSL_CLOSEST_HIT: LanguageSpec = GLSL_RAY_TRACING.copy(name = "GLSL (closest hit)")
val GLSL_ANY_HIT: LanguageSpec = GLSL_RAY_TRACING.copy(name = "GLSL (any hit)")
val GLSL_INTERSECTION: LanguageSpec = GLSL_RAY_TRACING.copy(name = "GLSL (intersection)")
val GLSL_CALLABLE: LanguageSpec = GLSL_RAY_TRACING.copy(name = "GLSL (callable)")
val SLANG_VERTEX: LanguageSpec
val SLANG_VERTEX: LanguageSpec = LanguageSpec(
name = "Slang (vertex)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_FRAGMENT: LanguageSpec
val SLANG_FRAGMENT: LanguageSpec = LanguageSpec(
name = "Slang (fragment)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_MESH: LanguageSpec
val SLANG_MESH: LanguageSpec = LanguageSpec(
name = "Slang (mesh)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_TASK: LanguageSpec
val SLANG_TASK: LanguageSpec = LanguageSpec(
name = "Slang (task)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_COMPUTE: LanguageSpec
val SLANG_COMPUTE: LanguageSpec = LanguageSpec(
name = "Slang (compute)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_VERTEX_FRAGMENT: LanguageSpec
val SLANG_VERTEX_FRAGMENT: LanguageSpec = LanguageSpec(
name = "Slang (vertex+fragment)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_RAY_TRACING: LanguageSpec
val SLANG_RAY_TRACING: LanguageSpec = LanguageSpec(
name = "Slang (ray generation)",
wrap = ::normalizeSlangSource,
unwrap = ::normalizeSlangSource
)
val SLANG_MISS: LanguageSpec = SLANG_RAY_TRACING.copy(name = "Slang (miss)")
val SLANG_CLOSEST_HIT: LanguageSpec = SLANG_RAY_TRACING.copy(name = "Slang (closest hit)")
val SLANG_ANY_HIT: LanguageSpec = SLANG_RAY_TRACING.copy(name = "Slang (any hit)")
val SLANG_INTERSECTION: LanguageSpec = SLANG_RAY_TRACING.copy(name = "Slang (intersection)")
val SLANG_CALLABLE: LanguageSpec = SLANG_RAY_TRACING.copy(name = "Slang (callable)")
private val GLSL_PROPERTY_SPECS: Map<String, LanguageSpec> = mapOf(
"vertex" to GLSL_VERTEX,
"fragment" to GLSL_FRAGMENT,
"mesh" to GLSL_MESH,
"task" to GLSL_TASK,
"compute" to GLSL_COMPUTE,
"raygen" to GLSL_RAY_TRACING,
"miss" to GLSL_MISS,
"closesthit" to GLSL_CLOSEST_HIT,
"anyhit" to GLSL_ANY_HIT,
"intersection" to GLSL_INTERSECTION,
"callable" to GLSL_CALLABLE
)
private val SLANG_PROPERTY_SPECS: Map<String, LanguageSpec> = mapOf(
"vertex_s" to SLANG_VERTEX,
"fragment_s" to SLANG_FRAGMENT,
"vertex_fragment" to SLANG_VERTEX_FRAGMENT,
"mesh_s" to SLANG_MESH,
"task_s" to SLANG_TASK,
"compute_s" to SLANG_COMPUTE,
"raygen_s" to SLANG_RAY_TRACING,
"miss_s" to SLANG_MISS,
"closesthit_s" to SLANG_CLOSEST_HIT,
"anyhit_s" to SLANG_ANY_HIT,
"intersection_s" to SLANG_INTERSECTION,
"callable_s" to SLANG_CALLABLE
)
private val SHADER_PROPERTY_SPECS: Map<String, LanguageSpec> =
GLSL_PROPERTY_SPECS + SLANG_PROPERTY_SPECS
fun glslSpecForProperty(property: String): LanguageSpec?
fun glslSpecForProperty(property: String): LanguageSpec? =
GLSL_PROPERTY_SPECS[property.lowercase()]
fun shaderSpecForProperty(property: String): LanguageSpec?
fun shaderSpecForProperty(property: String): LanguageSpec? =
SHADER_PROPERTY_SPECS[property.lowercase()]
val NANO_ASM: LanguageSpec
Language spec for the Nano micro-computer assembly language. Nano is an 8-bit VM with 4 registers (r0–r3) and 32 bytes of RAM, designed for emergence and artificial-life simulations.
val NANO_ASM: LanguageSpec = LanguageSpec(
name = "Nano",
wrap = { it.trimEnd() },
unwrap = { it.trimEnd() }
)