Skip to content

StackSLVocabulary.kt

src/jvmMain/kotlin/afield/lang/stacksl/StackSLVocabulary.kt · 118 lines

package afield.lang.stacksl

The words of a small language as an editor offers them: names, stack effects and descriptions read from the language's generated words guide, served as completions.

vocabularyFromGuide reads the guide's tables, vocabularyCompletion offers the words at a cursor, and StackSLVocabulary is StackSL's own list. StackSLVocabularyCoverageTest and LanguageVocabularyTest cover it.

import afield.app.protocol.Completion
import afield.app.protocol.Metadata

data class VocabularyWord(val word: String, val effect: String, val detail: String)

One word of a small language as an editor offers it: effect as its cheatsheet writes it (t a b c d -- color), and detail the cheatsheet's prose, as Markdown. Either may be empty.

fun vocabularyFromGuide(words: Collection<String>, guide: String): List<VocabularyWord>

words, each with what guide — a words guide generated from a language's cheatsheet — says it does, alphabetically. A table whose header has a Stack, Pushes, Reads or Stack / reads column gives the effect there, and its last column is the description; a row naming several words (`sat`, `saturate`) describes each of them. A word the guide does not describe is kept, with nothing said about it.

fun vocabularyFromGuide(words: Collection<String>, guide: String): List<VocabularyWord> {
    val described = HashMap<String, Pair<String, String>>()
    var effectColumn = -1
    for (line in guide.lines()) {
        if (!line.startsWith("|")) { effectColumn = -1; continue }
        val cells = line.trim().removePrefix("|").removeSuffix("|").split(" | ").map { it.trim() }
        if (cells.size < 2 || cells.all { it.matches(Regex("-+")) }) continue
        if (cells[0] in HEADER_FIRST_CELLS) {
            effectColumn = cells.indexOfFirst { it in EFFECT_HEADERS }
            continue
        }
        val effect = if (effectColumn in 1 until cells.size) cells[effectColumn].replace("`", "") else ""
        // Backticks stay: the description is Markdown, and `b*cos(t*c)` outside them is emphasis.
        val detail = if (effectColumn == cells.lastIndex) "" else cells.last()
        Regex("`([^`]+)`").findAll(cells[0]).forEach { described.putIfAbsent(it.groupValues[1], effect to detail) }
    }
    return words.toSortedSet().map { word ->
        val (effect, detail) = described[word] ?: ("" to "")
        VocabularyWord(word, effect, detail)
    }
}

fun vocabularyCompletion(source: String, position: Int, language: String, vocabulary: List<VocabularyWord>, ownWords: List<Metadata>): Completion

What Field's editor offers at position in source, a program in language: every word of vocabulary, and every name the program gives itself (ownWords), that begins with the word being typed — which runs back from position to whitespace or a bracket. Nothing inside a # comment or a string. Each row carries the word's stack effect as its tail and the cheatsheet's description.

fun vocabularyCompletion(source: String, position: Int, language: String, vocabulary: List<VocabularyWord>, ownWords: List<Metadata>): Completion {
    val end = position.coerceIn(0, source.length)
    var start = end
    while (start > 0 && !source[start - 1].isWhitespace() && source[start - 1] != '(' && source[start - 1] != ')') start--
    val prefix = source.substring(start, end)
    val lineSoFar = source.substring(source.lastIndexOf('\n', end - 1) + 1, end)
    if ('#' in lineSoFar || prefix.startsWith("\"") || lineSoFar.count { it == '"' } % 2 == 1) return Completion(start, end, metadata = emptyList())
    val lower = prefix.lowercase()
    val own = ownWords.filter { it.text.lowercase().startsWith(lower) }
    val words = vocabulary.filter { it.word.lowercase().startsWith(lower) && own.none { o -> o.text == it.word } }.map { w ->
        Metadata(icon = "keyword", tail = w.effect, text = w.word, displayText = w.word, category = "words",
            documentation = "**${w.word}**" + (if (w.effect.isNotBlank()) " `${w.effect}`" else "") +
                (if (w.detail.isNotBlank()) "\n\n${w.detail}" else ""),
            documentationSourceName = "$language cheatsheet")
    }
    return Completion(start, end, metadata = own + words)
}

private val HEADER_FIRST_CELLS = setOf("Word", "Words", "Syntax")
private val EFFECT_HEADERS = setOf("Stack", "Pushes", "Reads", "Stack / reads")

object StackSLVocabulary

Every word the StackSL compiler accepts that a person would type, with what the cheatsheet says it does — the vocabulary the editor offers in completion and explains beside the cursor.

The editor's own tables (stacksl.js) are written by hand and had drifted: on 2026-09-27 they documented 159 of the compiler's 314 words and offered no others, so palette, fbm3, min and max never appeared in completion. The words here come from the compiler itself (stackSLDocumentableWords); their stack effect and description from the StackSL words guide, which tools/stacksl_conversation_documents.py generates from public/docs/stacksl_cheatsheet.md and holds equal to it. The editor asks for completion through Field's complete request, which a StackSL program answers here (StackSLExecution), as does a Kotlin box for its stackSL property.

object StackSLVocabulary {

val words: List<VocabularyWord> by lazy

Every documentable word, alphabetically.

    val words: List<VocabularyWord> by lazy {
        vocabularyFromGuide(stackSLDocumentableWords,
            StackSLVocabulary::class.java.getResource("/afield/stacksl/study/stacksl_words.md")?.readText().orEmpty())
    }

fun complete(source: String, position: Int): Completion

Completion at position in the StackSL program source: the vocabulary, the program's own : name definitions, and <name for every mark it stores with >name.

    fun complete(source: String, position: Int): Completion {
        val lines = source.lines()
        fun lineOf(offset: Int) = source.substring(0, offset).count { it == '\n' } + 1
        val own = ArrayList<Metadata>()
        Regex("""(?:^|\s):\s+(\S+)""").findAll(source).forEach { m ->
            val line = lineOf(m.groups[1]!!.range.first)
            own += Metadata(icon = "function", tail = "definition", text = m.groupValues[1], displayText = m.groupValues[1],
                documentation = "**${m.groupValues[1]}**, defined on line $line:\n\n`${lines.getOrElse(line - 1) { "" }.trim().take(120)}`")
        }
        Regex("""(?:^|\s)>([A-Za-z_][A-Za-z0-9_-]*)""").findAll(source).map { it.groupValues[1] }.distinct()
            .filter { it !in setOf("x", "y", "z", "w") }.forEach { mark ->
                own += Metadata(icon = "variable", tail = "mark", text = "<$mark", displayText = "<$mark",
                    documentation = "**<$mark** `-- value`\n\nThe value this program stores in the mark `$mark` with `>$mark`.")
            }
        return vocabularyCompletion(source, position, "StackSL", words, own.distinctBy { it.text })
    }
}