fix(tools): formatter bracket rules, LSP compiler diagnostics, vocabulary sync

- ludic-fmt: `rows[i]`, `new []int`, `s[a..b]`, `emit(`, `~x`, list-literal
  and query-tag braces stay tight; member calls hug their paren
  (`Date.new(`, `Prefab.spawn(`); `<< >> & | ^ ~` are operators and
  `&& ||` are not — mirrored in ludic_syntax.h, LudicLexer.kt and the
  TextMate grammar. 149 of 274 tracked sources failed --check before.
- ludic-lsp: `initializationOptions.compilerDiagnostics` / `compilerPath`
  are honoured — on save the document's compilation unit is compiled and
  `file:line: error: msg` is published as a "ludicc" diagnostic (the
  option had been documented but never implemented); the workspace scan
  file is per process; bracket codes spelled as char literals
- Overlay added to LUDIC_PHASES, LudicTokens.kt, ludic-mode.el and the
  grammar (the game uses it; check-vocabulary flagged the drift)
- editors: VS Code snippets/package.json/extension.js (`${workspaceFolder}`
  and `~` expansion, PATH lookup, Apache-2.0, real repo URL), Neovim root
  markers, Helix/Zed/Sublime bin/ paths, Emacs vocabulary, JetBrains
  comments; tools/editors/README.md no longer describes C tooling
- .forgejo workflows clone `${{ github.server_url }}/${{ github.repository }}`
  so a fork or mirror tests itself; the docs publish pushes the same way

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 01:12:26 +03:00
parent 8fed9add66
commit e3e1bc7784
26 changed files with 746 additions and 586 deletions

View file

@ -25,10 +25,13 @@ jobs:
clang-16 --version | head -1 clang-16 --version | head -1
- name: Check out the triggering commit - name: Check out the triggering commit
env:
# the repository that triggered the run, so a fork or a mirror tests itself
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
run: | run: |
set -eu set -eu
git config --global --add safe.directory '*' git config --global --add safe.directory '*'
git clone https://git.workshopsoft.io/workshopsoft/ludic.git . git clone "$REPO_URL" .
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
git log --oneline -1 git log --oneline -1
# See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC. # See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC.

View file

@ -33,10 +33,13 @@ jobs:
clang-16 --version | head -1 clang-16 --version | head -1
- name: Check out the triggering commit - name: Check out the triggering commit
env:
# the repository that triggered the run, so a fork or a mirror tests itself
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
run: | run: |
set -eu set -eu
git config --global --add safe.directory '*' git config --global --add safe.directory '*'
git clone https://git.workshopsoft.io/workshopsoft/ludic.git . git clone "$REPO_URL" .
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
git log --oneline -1 git log --oneline -1
# The toolchain is macOS-first; on this Linux runner it links against a # The toolchain is macOS-first; on this Linux runner it links against a

View file

@ -17,13 +17,12 @@ jobs:
steps: steps:
- name: Check out with history - name: Check out with history
env: env:
BEFORE: ${{ github.event.before }} REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
BASE: ${{ github.base_ref }}
run: | run: |
set -eu set -eu
git config --global --add safe.directory '*' git config --global --add safe.directory '*'
# Full clone so both endpoints of the range are present. # Full clone so both endpoints of the range are present.
git clone https://git.workshopsoft.io/workshopsoft/ludic.git . git clone "$REPO_URL" .
git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}"
- name: Lint the new commits - name: Lint the new commits

View file

@ -41,11 +41,11 @@ jobs:
- name: Generate the documentation site - name: Generate the documentation site
env: env:
SOURCE_REF: ${{ github.ref_name }} SOURCE_REF: ${{ github.ref_name }}
REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
run: | run: |
set -eu set -eu
git config --global --add safe.directory '*' git config --global --add safe.directory '*'
git clone --depth 1 --branch "${SOURCE_REF:-main}" \ git clone --depth 1 --branch "${SOURCE_REF:-main}" "$REPO_URL" src
https://git.workshopsoft.io/workshopsoft/ludic.git src
cd src cd src
# The toolchain is macOS-first; on this Linux runner it links against a # The toolchain is macOS-first; on this Linux runner it links against a
# tiny C-free IR shim supplying the Darwin stdout/stderr globals over # tiny C-free IR shim supplying the Darwin stdout/stderr globals over
@ -67,6 +67,8 @@ jobs:
PAGES_TOKEN: ${{ secrets.PAGES_TOKEN }} PAGES_TOKEN: ${{ secrets.PAGES_TOKEN }}
AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }} AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SOURCE_SHA: ${{ github.sha }} SOURCE_SHA: ${{ github.sha }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
run: | run: |
set -eu set -eu
TOKEN="${PAGES_TOKEN:-${AUTO_TOKEN:-}}" TOKEN="${PAGES_TOKEN:-${AUTO_TOKEN:-}}"
@ -80,5 +82,6 @@ jobs:
git config user.email "docs@workshopsoft.io" git config user.email "docs@workshopsoft.io"
git add -A git add -A
git commit -q -m "docs: regenerate site from ${SOURCE_SHA}" git commit -q -m "docs: regenerate site from ${SOURCE_SHA}"
git push -f "https://ludic-docs-bot:${TOKEN}@git.workshopsoft.io/workshopsoft/ludic.git" pages # the same server and repository the run came from, with the token spliced in
git push -f "${SERVER_URL%%://*}://ludic-docs-bot:${TOKEN}@${SERVER_URL#*://}/${REPO}.git" pages
echo "published $(git rev-parse --short HEAD) to pages" echo "published $(git rev-parse --short HEAD) to pages"

View file

@ -1,20 +1,18 @@
# Ludic in your editor # Ludic in your editor
Everything here is built on one idea: **write the language knowledge once, in C, Everything here is built on one idea: **write the language knowledge once, in
and let every editor talk to it.** There is one lexer, one vocabulary, one Ludic, and let every editor talk to it.** There is one vocabulary, one
formatter and one language server. VS Code and JetBrains get first-class plugins formatter and one language server. VS Code and JetBrains get first-class plugins
because those are the two that were asked for; every other editor gets the same because those are the two that were asked for; every other editor gets the same
capabilities by pointing at the same binary. capabilities by pointing at the same binary.
``` ```
tools/ludic-tools/ the actual language knowledge, in C, no dependencies tools/ludic-tools/ the actual language knowledge, in Ludic
ludic_syntax.h lexer + the vocabulary — the source of truth ludic_syntax.h the vocabulary tables (keywords, operators, builtins)
ludic_fmt.h comment-preserving formatter that the formatter, the server and the vocabulary
ludic_index.h error-tolerant reader: declarations, bindings, scopes check all read — the source of truth
ludic_workspace.h multi-file model: imports, compilation units, Markdown fmt.ludic -> bin/ludic-fmt token-based, comment-preserving formatter
ludic_json.h just enough JSON for LSP lsp.ludic -> bin/ludic-lsp the language server (LSP 3.17)
ludic_fmt_main.c -> bin/ludic-fmt
ludic_lsp.c -> bin/ludic-lsp
tools/editors/ tools/editors/
shared/ TextMate grammar + Markdown injection + language config shared/ TextMate grammar + Markdown injection + language config

View file

@ -10,19 +10,23 @@
;;; Code: ;;; Code:
;; The vocabulary mirrors tools/ludic-tools/ludic_syntax.h; that header is the reference.
(defconst ludic--declaration-keywords (defconst ludic--declaration-keywords
'("program" "import" "property" "struct" "model" "enum" "ui" '("program" "import" "property" "model" "enum" "ui" "namespace"
"const" "var" "fn" "extern" "handler" "entry")) "const" "var" "function" "extern" "handler" "entry" "event" "scene" "test" "prefab"))
(defconst ludic--clause-keywords (defconst ludic--clause-keywords
'("phase" "query" "reads" "writes")) '("phase" "query" "on" "cancellable" "public" "layer" "start" "shows" "lasts" "loads" "then" "export" "internal"))
(defconst ludic--statement-keywords (defconst ludic--statement-keywords
'("let" "return" "if" "else" "when" "while" "for" "in" "spawn" "despawn" '("let" "return" "if" "else" "while" "for" "in" "spawn" "despawn"
"match" "machine" "state" "become" "enter" "where" "and" "or")) "enable" "disable" "attach" "detach" "match" "machine" "state" "become" "where"
"and" "or" "not" "break" "continue" "new" "emit" "cancel" "try"))
(defconst ludic--types '("int" "fixed" "bool" "entity" "str" "ptr" "void")) (defconst ludic--types
(defconst ludic--phases '("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render")) '("int" "long" "fixed" "countdown" "bool" "entity" "string" "pointer" "byte"
"words" "fixeds" "pointers" "Vector" "IVec2" "Rect" "void"))
(defconst ludic--phases '("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render" "Overlay"))
(defconst ludic-font-lock-keywords (defconst ludic-font-lock-keywords
(list (list
@ -31,7 +35,7 @@
(cons (regexp-opt ludic--statement-keywords 'symbols) 'font-lock-keyword-face) (cons (regexp-opt ludic--statement-keywords 'symbols) 'font-lock-keyword-face)
(cons (regexp-opt ludic--types 'symbols) 'font-lock-type-face) (cons (regexp-opt ludic--types 'symbols) 'font-lock-type-face)
(cons (regexp-opt ludic--phases 'symbols) 'font-lock-builtin-face) (cons (regexp-opt ludic--phases 'symbols) 'font-lock-builtin-face)
(cons "\\_<\\(true\\|false\\)\\_>" 'font-lock-constant-face) (cons "\\_<\\(true\\|false\\|null\\)\\_>" 'font-lock-constant-face)
(cons "@[A-Za-z_][A-Za-z0-9_]*" 'font-lock-preprocessor-face) (cons "@[A-Za-z_][A-Za-z0-9_]*" 'font-lock-preprocessor-face)
;; components, archetypes, scenes and UI handles are Capitalised by convention ;; components, archetypes, scenes and UI handles are Capitalised by convention
(cons "\\_<[A-Z][A-Za-z0-9_]*\\_>" 'font-lock-type-face) (cons "\\_<[A-Z][A-Za-z0-9_]*\\_>" 'font-lock-type-face)

View file

@ -10,7 +10,7 @@ comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-servers = ["ludic-lsp"] language-servers = ["ludic-lsp"]
auto-format = true auto-format = true
roots = ["build.sh", "LANGUAGE.md", ".git"] roots = ["package.ludic", "LANGUAGE.md", ".git"]
[language-server.ludic-lsp] [language-server.ludic-lsp]
command = "ludic-lsp" command = "ludic-lsp"

View file

@ -35,7 +35,7 @@ class LudicBraceMatcher : PairedBraceMatcher {
private companion object { private companion object {
// Structural braces only: a record literal shares the token type, so // Structural braces only: a record literal shares the token type, so
// marking them all structural would make the "matched brace" gutter // marking them all structural would make the "matched brace" gutter
// noisy inside `spawn Hero { Pos = { x = 1 } }`. // noisy inside `spawn Hero { Pos { x: 1 } }`.
val PAIRS = arrayOf( val PAIRS = arrayOf(
BracePair(LudicTokens.BRACE, LudicTokens.BRACE, true), BracePair(LudicTokens.BRACE, LudicTokens.BRACE, true),
BracePair(LudicTokens.BRACKET, LudicTokens.BRACKET, false), BracePair(LudicTokens.BRACKET, LudicTokens.BRACKET, false),

View file

@ -7,7 +7,7 @@ import com.intellij.psi.tree.IElementType
/** /**
* A hand-written lexer matching tools/ludic-tools/ludic_syntax.h token for * A hand-written lexer matching tools/ludic-tools/ludic_syntax.h token for
* token. Hand-written rather than generated because the grammar is small, the * token. Hand-written rather than generated because the grammar is small, the
* rules are the ones the C lexer already implements, and keeping the two * rules are the ones the shared header already implements, and keeping the two
* readable side by side is worth more here than a JFlex spec. * readable side by side is worth more here than a JFlex spec.
* *
* It must never throw and never fail to advance: the IDE lexes half-typed text * It must never throw and never fail to advance: the IDE lexes half-typed text
@ -142,7 +142,7 @@ class LudicLexer : LexerBase() {
} }
private companion object { private companion object {
val TWO_CHAR_OPS = setOf("->", "+=", "-=", "*=", "/=", "==", "!=", "<=", ">=", "&&", "||", "..", "=>") val TWO_CHAR_OPS = setOf("->", "+=", "-=", "*=", "/=", "==", "!=", "<=", ">=", "<<", ">>", "..", "=>")
const val SINGLE_CHAR_OPS = "+-*/%<>=:.!@;" const val SINGLE_CHAR_OPS = "+-*/%<>=:.!@;&|^~"
} }
} }

View file

@ -17,7 +17,7 @@ import java.io.File
/** /**
* Where to find the toolchain. Both paths may be left blank, in which case the * Where to find the toolchain. Both paths may be left blank, in which case the
* plugin looks under the project root for what tools/build-tools.sh produces — * plugin looks under the project root for what `bin/x tools` produces —
* which is the case for anyone working in the Ludic repository itself. * which is the case for anyone working in the Ludic repository itself.
*/ */
@Service(Service.Level.PROJECT) @Service(Service.Level.PROJECT)
@ -110,7 +110,7 @@ class LudicConnectionProvider(private val project: Project) : ProcessStreamConne
} }
append(" and on PATH.\n") append(" and on PATH.\n")
} }
append("\nBuild it with ./tools/build-tools.sh from the repository root, ") append("\nBuild it with `bin/x tools` from the repository root, ")
append("or set the path in Settings | Languages & Frameworks | Ludic. ") append("or set the path in Settings | Languages & Frameworks | Ludic. ")
append("Syntax highlighting works without it; completion and diagnostics do not.") append("Syntax highlighting works without it; completion and diagnostics do not.")
} }

View file

@ -20,8 +20,8 @@ object LudicTokens {
val IDENTIFIER = LudicTokenType("IDENTIFIER") val IDENTIFIER = LudicTokenType("IDENTIFIER")
val TYPE_NAME = LudicTokenType("TYPE_NAME") // Capitalised: component, archetype, scene val TYPE_NAME = LudicTokenType("TYPE_NAME") // Capitalised: component, archetype, scene
val KEYWORD = LudicTokenType("KEYWORD") val KEYWORD = LudicTokenType("KEYWORD")
val DECL_KEYWORD = LudicTokenType("DECL_KEYWORD") // game, component, system, function, ... val DECL_KEYWORD = LudicTokenType("DECL_KEYWORD") // program, property, model, handler, function, ...
val CLAUSE = LudicTokenType("CLAUSE") // phase, query, reads, writes, ... val CLAUSE = LudicTokenType("CLAUSE") // phase, query, on, layer, ...
val PRIMITIVE = LudicTokenType("PRIMITIVE") // int, fixed, bool, entity, string, pointer, void val PRIMITIVE = LudicTokenType("PRIMITIVE") // int, fixed, bool, entity, string, pointer, void
val PHASE = LudicTokenType("PHASE") // Start, Update, Render, ... val PHASE = LudicTokenType("PHASE") // Start, Update, Render, ...
val BOOLEAN = LudicTokenType("BOOLEAN") val BOOLEAN = LudicTokenType("BOOLEAN")
@ -53,7 +53,7 @@ object LudicVocabulary {
"and", "or", "not", "break", "continue", "new", "emit", "cancel", "try" "and", "or", "not", "break", "continue", "new", "emit", "cancel", "try"
) )
val PRIMITIVES = setOf("int", "long", "fixed", "countdown", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "pointers", "Vector", "IVec2", "Rect", "void") val PRIMITIVES = setOf("int", "long", "fixed", "countdown", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "pointers", "Vector", "IVec2", "Rect", "void")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render") val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render", "Overlay")
val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer") val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer")
val BUILTINS = setOf( val BUILTINS = setOf(

View file

@ -67,7 +67,7 @@
name="Ludic" name="Ludic"
factoryClass="io.ludic.ide.LudicLanguageServerFactory"> factoryClass="io.ludic.ide.LudicLanguageServerFactory">
<description><![CDATA[ <description><![CDATA[
The Ludic language server. Built by <code>tools/build-tools.sh</code>. The Ludic language server. Built by <code>bin/x tools</code>.
]]></description> ]]></description>
</server> </server>
<languageMapping language="Ludic" serverId="ludic"/> <languageMapping language="Ludic" serverId="ludic"/>

View file

@ -3,7 +3,7 @@
-- :lua require('ludic').setup() -- :lua require('ludic').setup()
-- -- or, from init.lua: -- -- or, from init.lua:
-- vim.opt.runtimepath:append('/path/to/gpp/tools/editors/neovim') -- vim.opt.runtimepath:append('/path/to/gpp/tools/editors/neovim')
-- require('ludic').setup({ server = '/path/to/gpp/build/ludic-lsp' }) -- require('ludic').setup({ server = '/path/to/gpp/bin/ludic-lsp' })
-- --
-- Highlighting here comes from the language server's semantic tokens, which -- Highlighting here comes from the language server's semantic tokens, which
-- Neovim applies natively -- there is no tree-sitter grammar to install and no -- Neovim applies natively -- there is no tree-sitter grammar to install and no
@ -43,7 +43,7 @@ function M.setup(opts)
end, end,
}) })
local root_markers = { 'build.sh', 'LANGUAGE.md', '.git' } local root_markers = { 'package.ludic', 'LANGUAGE.md', '.git' }
local function root_dir(fname) local function root_dir(fname)
local found = vim.fs.find(root_markers, { path = vim.fs.dirname(fname), upward = true })[1] local found = vim.fs.find(root_markers, { path = vim.fs.dirname(fname), upward = true })[1]
@ -57,10 +57,10 @@ function M.setup(opts)
callback = function(args) callback = function(args)
local root = root_dir(vim.api.nvim_buf_get_name(args.buf)) local root = root_dir(vim.api.nvim_buf_get_name(args.buf))
local server = opts.server or find_executable( local server = opts.server or find_executable(
{ 'ludic-lsp' }, { root }, { 'build', 'tools/build', 'bin', '.' }) { 'ludic-lsp' }, { root }, { 'bin', 'build', '.' })
if not server then return end if not server then return end
local compiler = opts.compiler or find_executable( local compiler = opts.compiler or find_executable(
{ 'ludicc' }, { root }, { 'build', 'bin', '.' }) or '' { 'ludicc' }, { root }, { 'bin', 'build', '.' }) or ''
vim.lsp.start({ vim.lsp.start({
name = 'ludic-lsp', name = 'ludic-lsp',

View file

@ -12,7 +12,7 @@
], ],
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]], "surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
"indentationRules": { "indentationRules": {
"increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$", "increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
"decreaseIndentPattern": "^\\s*[}\\])].*$" "decreaseIndentPattern": "^\\s*[}\\])].*$"
}, },
"onEnterRules": [ "onEnterRules": [

View file

@ -133,12 +133,20 @@
} }
}, },
{ {
"match": "\\b(enter)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(become)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "keyword.control.enter.ludic" }, "1": { "name": "keyword.control.become.ludic" },
"2": { "name": "entity.name.type.scene.ludic" } "2": { "name": "entity.name.type.scene.ludic" }
} }
}, },
{
"match": "\\b(scene|prefab|event)\\s+(cancellable\\s+)?([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.ludic" },
"2": { "name": "keyword.other.clause.ludic" },
"3": { "name": "entity.name.type.ludic" }
}
},
{ {
"match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
@ -172,29 +180,29 @@
}, },
"keyword": { "keyword": {
"patterns": [ "patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|prefab|become|return|spawn|despawn|enable|disable|where|break|continue|new|try)\\b" }, { "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|namespace|enum|ui|const|var|let|function|handler|entry|state|test)\\b" }, { "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|handler|entry|event|scene|state|test)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" }, { "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" }, { "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" } { "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
] ]
}, },
"widget": { "widget": {
"patterns": [ "patterns": [
{ {
"comment": "widget types, recognised by the props or the child block that follow them", "comment": "widget types, recognised by the `key: value` props or the child block that follow them",
"name": "support.class.widget.ludic", "name": "support.class.widget.ludic",
"match": "\\b(panel|col|row|label|button|image|spacer)\\b(?=\\s*(\\{|[a-z][A-Za-z0-9_]*\\s*=|$))" "match": "\\b(panel|col|row|label|button|image|spacer)\\b(?=\\s*(\\{|[a-z][A-Za-z0-9_]*\\s*:|$))"
}, },
{ {
"comment": "widget props are written tight (id=Root); an assignment is written x = 1", "comment": "a widget prop or a named argument: `id: Root`, `x: 10`",
"match": "\\b([a-z][A-Za-z0-9_]*)(=)(?!=)", "match": "\\b([a-z][A-Za-z0-9_]*)(:)(?=\\s)",
"captures": { "captures": {
"1": { "name": "variable.parameter.widget.ludic" }, "1": { "name": "variable.parameter.ludic" },
"2": { "name": "keyword.operator.assignment.ludic" } "2": { "name": "punctuation.separator.ludic" }
} }
} }
] ]

View file

@ -20,10 +20,10 @@ Sublime reads TextMate grammars directly, so the shared grammar works as-is.
"clients": { "clients": {
"ludic": { "ludic": {
"enabled": true, "enabled": true,
"command": ["/path/to/gpp/build/ludic-lsp", "--stdio"], "command": ["/path/to/gpp/bin/ludic-lsp", "--stdio"],
"selector": "source.ludic | text.html.markdown", "selector": "source.ludic | text.html.markdown",
"initializationOptions": { "initializationOptions": {
"compilerPath": "/path/to/gpp/build/ludicc", "compilerPath": "/path/to/gpp/bin/ludicc",
"compilerDiagnostics": true, "compilerDiagnostics": true,
"indentSize": 2 "indentSize": 2
} }

View file

@ -37,9 +37,9 @@ Format on save:
## Settings ## Settings
- `ludic.server.path` — where `ludic-lsp` lives. Empty = search `bin/`, then `PATH`. - `ludic.server.path` — where `ludic-lsp` lives (`~` and `${workspaceFolder}` expand). Empty = search `bin/`, then `PATH`.
- `ludic.server.enabled` — turn the server off and keep only static highlighting. - `ludic.server.enabled` — turn the server off and keep only static highlighting.
- `ludic.compilerPath` — where `ludicc` lives, for on-save diagnostics. - `ludic.compilerPath` — where `ludicc` lives, for on-save diagnostics (`~` and `${workspaceFolder}` expand).
- `ludic.compilerDiagnostics` — whether to run the compiler at all. - `ludic.compilerDiagnostics` — whether to run the compiler at all.
- `ludic.indentSize` — spaces per level for the formatter. - `ludic.indentSize` — spaces per level for the formatter.
- `ludic.trace.server` — log LSP traffic (`Ludic: Show Language Server Log`). - `ludic.trace.server` — log LSP traffic (`Ludic: Show Language Server Log`).

View file

@ -12,7 +12,7 @@
], ],
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]], "surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
"indentationRules": { "indentationRules": {
"increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$", "increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
"decreaseIndentPattern": "^\\s*[}\\])].*$" "decreaseIndentPattern": "^\\s*[}\\])].*$"
}, },
"onEnterRules": [ "onEnterRules": [

View file

@ -4,7 +4,7 @@
"description": "Ludic language support: syntax highlighting, formatting, and the ludic-lsp language server. Also highlights ```ludic fences in Markdown.", "description": "Ludic language support: syntax highlighting, formatting, and the ludic-lsp language server. Also highlights ```ludic fences in Markdown.",
"version": "1.0.0", "version": "1.0.0",
"publisher": "ludic", "publisher": "ludic",
"license": "MIT", "license": "Apache-2.0",
"engines": { "engines": {
"vscode": "^1.75.0" "vscode": "^1.75.0"
}, },
@ -90,7 +90,7 @@
"ludic.server.path": { "ludic.server.path": {
"type": "string", "type": "string",
"default": "", "default": "",
"markdownDescription": "Path to the `ludic-lsp` binary. Empty means: look in `build/ludic-lsp` under each workspace folder, then on `PATH`." "markdownDescription": "Path to the `ludic-lsp` binary; `~` and `${workspaceFolder}` are expanded. Empty means: look in `bin/ludic-lsp` under each workspace folder, then on `PATH`."
}, },
"ludic.server.enabled": { "ludic.server.enabled": {
"type": "boolean", "type": "boolean",
@ -100,7 +100,7 @@
"ludic.compilerPath": { "ludic.compilerPath": {
"type": "string", "type": "string",
"default": "", "default": "",
"markdownDescription": "Path to `ludicc`. The server shells out to it for authoritative diagnostics on save. Empty means `build/ludicc` under the workspace." "markdownDescription": "Path to `ludicc`; `~` and `${workspaceFolder}` are expanded. The server runs it on save for the compiler's own diagnostics. Empty means `bin/ludicc` under the workspace, then `PATH`."
}, },
"ludic.compilerDiagnostics": { "ludic.compilerDiagnostics": {
"type": "boolean", "type": "boolean",
@ -153,6 +153,6 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://example.invalid/ludic" "url": "https://git.workshopsoft.io/workshopsoft/ludic"
} }
} }

View file

@ -1,53 +1,52 @@
{ {
"game": { "program": {
"prefix": "game", "prefix": "program",
"body": ["game ${1:Name} {", "\t$0", "}"], "body": ["program ${1:Name} {", "\t$0", "}"],
"description": "A game unit — compiles to an executable." "description": "A program: one block of declarations that compiles to an executable."
}, },
"module": { "property": {
"prefix": "module", "prefix": "property",
"body": ["module ${1:Name} {", "\t$0", "}"], "body": ["property ${1:Name} { ${2:x}: ${3:int} = ${4:0} }"],
"description": "A library unit — compiles to a shared library, no frame loop." "description": "A property: typed fields with defaults — a per-entity component, or a record made with `new`."
}, },
"component": { "model": {
"prefix": "component", "prefix": "model",
"body": ["component ${1:Name} { ${2:x}: ${3:int} = ${4:0} }"], "body": ["model ${1:Name} { ${2:Position} }"],
"description": "A component: typed per-entity fields." "description": "An entity kind bundling a fixed set of properties."
}, },
"archetype": { "prefab": {
"prefix": "archetype", "prefix": "prefab",
"body": ["archetype ${1:Name} {", "\t${2:Pos}", "}"], "body": ["prefab ${1:Name}: ${2:Model} { ${3:Stats} { ${4:hp}: ${5:30} } }"],
"description": "An entity kind bundling a fixed set of components." "description": "A model with preset property fields; `spawn` adds only what differs."
}, },
"system": { "handler": {
"prefix": "system", "prefix": "handler",
"body": [ "body": [
"system ${1:Name} phase ${2|Start,Input,FixedUpdate,Update,LateUpdate,Render|} {", "handler ${1:Name} phase ${2|Start,Input,FixedUpdate,Update,LateUpdate,Render,Overlay|} {",
"\t$0", "\t$0",
"}" "}"
], ],
"description": "A system that runs once per tick in its phase." "description": "A handler that runs once per tick in its phase."
}, },
"system with query": { "handler with query": {
"prefix": "systemq", "prefix": "handlerq",
"body": [ "body": [
"system ${1:Name} phase ${2|Update,FixedUpdate,LateUpdate,Render,Input|}", "@Queries(these: [${1:Position}])",
" query (${3:p}) [${4:Pos}]", "handler ${2:Name} phase ${3|Update,FixedUpdate,LateUpdate,Render,Input|} {",
"{",
"\t$0", "\t$0",
"}" "}"
], ],
"description": "A system whose body runs once per matching entity." "description": "A handler whose body runs once per matching entity, each property bound by name."
}, },
"for query": { "for query": {
"prefix": "forq", "prefix": "forq",
"body": ["for (${1:p}) in query [${2:Pos}] {", "\t$0", "}"], "body": ["for (${1:p}) in query [${2:Position}] {", "\t$0", "}"],
"description": "Iterate every entity matching a query." "description": "Iterate every entity matching a query."
}, },
"spawn": { "spawn": {
"prefix": "spawn", "prefix": "spawn",
"body": ["spawn ${1:Kind} {", "\t${2:Pos} = { ${3:x} = ${4:0} }", "}"], "body": ["spawn ${1:Kind} { ${2:Position} { ${3:x}: ${4:0} } }"],
"description": "Create an entity of an archetype." "description": "Create an entity of a model or prefab."
}, },
"scene": { "scene": {
"prefix": "scene", "prefix": "scene",
@ -59,18 +58,18 @@
"\t}", "\t}",
"}" "}"
], ],
"description": "A scene with lifecycle hooks and one layer of systems." "description": "A scene with lifecycle hooks and one layer of handlers."
}, },
"ui": { "ui": {
"prefix": "ui", "prefix": "ui",
"body": [ "body": [
"ui ${1:Name} {", "ui ${1:Name} {",
"\tpanel id=${2:Root} pad=16 gap=6 align=center {", "\tpanel id: ${2:Root} pad: 16 gap: 6 align: center {",
"\t\t$0", "\t\t$0",
"\t}", "\t}",
"}" "}"
], ],
"description": "A retained UI tree." "description": "A retained UI tree, declared as data."
}, },
"match": { "match": {
"prefix": "match", "prefix": "match",
@ -80,17 +79,32 @@
"machine": { "machine": {
"prefix": "machine", "prefix": "machine",
"body": [ "body": [
"machine reg(${1:R_PHASE}) {", "machine ${1:state} {",
"\tstate ${2:Idle} = 0 {", "\tstate ${2:Idle} {",
"\t\t$0", "\t\t$0",
"\t}", "\t}",
"}" "}"
], ],
"description": "A state machine over a register." "description": "A state machine over an enum-typed variable; `become` transitions."
}, },
"extern fn": { "event": {
"prefix": "event",
"body": ["event ${1:Name} { ${2:amount}: int = 0 }"],
"description": "An event payload; fire it with `emit`, listen with `@On`."
},
"listener": {
"prefix": "on",
"body": ["@On(${1:Event}) handler ${2:Name} { $0 }"],
"description": "A listener on an event, its payload fields bound by name."
},
"namespace": {
"prefix": "namespace",
"body": ["namespace ${1:Name} {", "\texport function ${2:method}() -> ${3:void} { $0 }", "}"],
"description": "A block of functions callable as Name.method(); `internal` keeps a helper private."
},
"extern function": {
"prefix": "extern", "prefix": "extern",
"body": ["extern fn ${1:name}(${2:a}: ${3:int}) -> ${4:int} = \"${5:c_symbol}\""], "body": ["extern function ${1:name}(${2:a}: ${3:int}) -> ${4:int} = \"${5:c_symbol}\""],
"description": "Bind a C symbol through the FFI." "description": "Bind a C symbol through the FFI."
} }
} }

View file

@ -19,7 +19,7 @@ let output = null;
/** Where the language server might be, in the order we should look. */ /** Where the language server might be, in the order we should look. */
function resolveServer(folders) { function resolveServer(folders) {
const configured = vscode.workspace.getConfiguration('ludic').get('server.path'); const configured = vscode.workspace.getConfiguration('ludic').get('server.path');
if (configured) return expand(configured); if (configured) return expand(configured, folders);
const names = ['ludic-lsp', 'ludic-lsp.exe']; const names = ['ludic-lsp', 'ludic-lsp.exe'];
for (const folder of folders) { for (const folder of folders) {
@ -43,18 +43,26 @@ function resolveServer(folders) {
function resolveCompiler(folders) { function resolveCompiler(folders) {
const configured = vscode.workspace.getConfiguration('ludic').get('compilerPath'); const configured = vscode.workspace.getConfiguration('ludic').get('compilerPath');
if (configured) return expand(configured); if (configured) return expand(configured, folders);
for (const folder of folders) { for (const folder of folders) {
for (const rel of ['bin', 'build']) { for (const rel of ['bin', 'build']) {
const p = path.join(folder.uri.fsPath, rel, 'ludicc'); const p = path.join(folder.uri.fsPath, rel, 'ludicc');
if (isExecutable(p)) return p; if (isExecutable(p)) return p;
} }
} }
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
const p = path.join(dir, 'ludicc');
if (isExecutable(p)) return p;
}
return ''; return '';
} }
function expand(p) { /** A configured path may use `~` and `${workspaceFolder}`, so a project can point
return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p; * at a sibling checkout without hard-coding one machine's home directory. */
function expand(p, folders) {
const root = folders && folders.length ? folders[0].uri.fsPath : '';
const withRoot = p.replace(/\$\{workspaceFolder\}/g, root);
return withRoot.startsWith('~') ? path.join(os.homedir(), withRoot.slice(1)) : withRoot;
} }
function isExecutable(p) { function isExecutable(p) {

View file

@ -133,12 +133,20 @@
} }
}, },
{ {
"match": "\\b(enter)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(become)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
"1": { "name": "keyword.control.enter.ludic" }, "1": { "name": "keyword.control.become.ludic" },
"2": { "name": "entity.name.type.scene.ludic" } "2": { "name": "entity.name.type.scene.ludic" }
} }
}, },
{
"match": "\\b(scene|prefab|event)\\s+(cancellable\\s+)?([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.ludic" },
"2": { "name": "keyword.other.clause.ludic" },
"3": { "name": "entity.name.type.ludic" }
}
},
{ {
"match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)", "match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": { "captures": {
@ -172,29 +180,29 @@
}, },
"keyword": { "keyword": {
"patterns": [ "patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|prefab|become|return|spawn|despawn|enable|disable|where|break|continue|new|try)\\b" }, { "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|namespace|enum|ui|const|var|let|function|handler|entry|state|test)\\b" }, { "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|handler|entry|event|scene|state|test)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" }, { "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" }, { "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" } { "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
] ]
}, },
"widget": { "widget": {
"patterns": [ "patterns": [
{ {
"comment": "widget types, recognised by the props or the child block that follow them", "comment": "widget types, recognised by the `key: value` props or the child block that follow them",
"name": "support.class.widget.ludic", "name": "support.class.widget.ludic",
"match": "\\b(panel|col|row|label|button|image|spacer)\\b(?=\\s*(\\{|[a-z][A-Za-z0-9_]*\\s*=|$))" "match": "\\b(panel|col|row|label|button|image|spacer)\\b(?=\\s*(\\{|[a-z][A-Za-z0-9_]*\\s*:|$))"
}, },
{ {
"comment": "widget props are written tight (id=Root); an assignment is written x = 1", "comment": "a widget prop or a named argument: `id: Root`, `x: 10`",
"match": "\\b([a-z][A-Za-z0-9_]*)(=)(?!=)", "match": "\\b([a-z][A-Za-z0-9_]*)(:)(?=\\s)",
"captures": { "captures": {
"1": { "name": "variable.parameter.widget.ludic" }, "1": { "name": "variable.parameter.ludic" },
"2": { "name": "keyword.operator.assignment.ludic" } "2": { "name": "punctuation.separator.ludic" }
} }
} }
] ]

View file

@ -1,9 +1,9 @@
# Ludic for Zed # Ludic for Zed
Zed extensions want a Tree-sitter grammar for highlighting, which this toolchain Zed extensions want a Tree-sitter grammar for highlighting, which this toolchain
deliberately does not ship — the vocabulary lives in one C header and is exposed deliberately does not ship — the vocabulary lives in one shared header and is
through the language server's semantic tokens instead, so there is no second exposed through the language server's semantic tokens instead, so there is no
grammar to keep honest. second grammar to keep honest.
That is enough for Zed: point it at `ludic-lsp` and highlighting, completion, That is enough for Zed: point it at `ludic-lsp` and highlighting, completion,
diagnostics and formatting all arrive over LSP. diagnostics and formatting all arrive over LSP.
@ -14,9 +14,9 @@ Add to `~/.config/zed/settings.json`:
{ {
"lsp": { "lsp": {
"ludic-lsp": { "ludic-lsp": {
"binary": { "path": "/path/to/gpp/build/ludic-lsp", "arguments": ["--stdio"] }, "binary": { "path": "/path/to/gpp/bin/ludic-lsp", "arguments": ["--stdio"] },
"initialization_options": { "initialization_options": {
"compilerPath": "/path/to/gpp/build/ludicc", "compilerPath": "/path/to/gpp/bin/ludicc",
"compilerDiagnostics": true, "compilerDiagnostics": true,
"indentSize": 2 "indentSize": 2
} }

View file

@ -40,27 +40,27 @@ program LudicFmt {
file_close(f) file_close(f)
return buf return buf
} }
function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n } function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n }
function char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 } function char_is_digit(c: int) -> bool { return c >= '0' and c <= '9' }
function char_is_alpha(c: int) -> bool { function char_is_alpha(c: int) -> bool {
if c >= 65 and c <= 90 { return true } if c >= 'A' and c <= 'Z' { return true }
if c >= 97 and c <= 122 { return true } if c >= 'a' and c <= 'z' { return true }
return c == 95 return c == '_'
} }
function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) }
function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) } function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F') }
function itoa(v: int) -> pointer { function itoa(v: int) -> pointer {
if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z } if v == 0 { let z = bytes(2); z[0] = '0'; z[1] = 0; return z }
var neg = false; var x = v var neg = false; var x = v
if x < 0 { neg = true; x = 0 - x } if x < 0 { neg = true; x = -x }
let tmp = bytes(16); var n = 0 let tmp = bytes(16); var n = 0
while x > 0 { tmp[n] = 48 + x % 10; x = x / 10; n = n + 1 } while x > 0 { tmp[n] = 48 + x % 10; x /= 10; n += 1 }
var total = n var total = n
if neg { total = total + 1 } if neg { total += 1 }
let out = bytes(total + 1); var k = 0 let out = bytes(total + 1); var k = 0
if neg { out[0] = 45; k = 1 } if neg { out[0] = '-'; k = 1 }
var i = 0 var i = 0
while i < n { out[k + i] = tmp[n - 1 - i]; i = i + 1 } while i < n { out[k + i] = tmp[n - 1 - i]; i += 1 }
out[total] = 0 out[total] = 0
return out return out
} }
@ -70,15 +70,15 @@ program LudicFmt {
function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b } function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b }
function buf_ensure(b: Buf, extra: int) -> void { function buf_ensure(b: Buf, extra: int) -> void {
if b.len + extra + 1 <= b.cap { return } if b.len + extra + 1 <= b.cap { return }
while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 } while b.len + extra + 1 > b.cap { b.cap *= 2 }
b.data = resize(b.data, b.cap) b.data = resize(b.data, b.cap)
} }
function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 } function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len += 1 }
function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } } function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i += 1 } }
function buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, 32); i = i + 1 } } function buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, ' '); i += 1 } }
function buf_str(b: Buf) -> pointer { b.data[b.len] = 0; return b.data } function buf_str(b: Buf) -> pointer { b.data[b.len] = 0; return b.data }
# append src[a..b) raw # append src[a..b) raw
function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } } function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k += 1 } }
# ---- the token stream (parallel slices) ---- # ---- the token stream (parallel slices) ----
var src: pointer = null var src: pointer = null
@ -117,16 +117,19 @@ program LudicFmt {
# ---- the lexer: keeps comments, newlines, byte spans; never exits on bad input ---- # ---- the lexer: keeps comments, newlines, byte spans; never exits on bad input ----
function is_op2(c0: int, c1: int) -> bool { function is_op2(c0: int, c1: int) -> bool {
if c0 == 45 and c1 == 62 { return true } # -> if c0 == '-' and c1 == '>' { return true } # ->
if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } # += -= *= /= == != <= >= if c1 == '=' and (c0 == '+' or c0 == '-' or c0 == '*' or c0 == '/' or c0 == '=' or c0 == '!' or c0 == '<' or c0 == '>') { return true } # += -= *= /= == != <= >=
if c0 == 38 and c1 == 38 { return true } # && if c0 == '<' and c1 == '<' { return true } # <<
if c0 == 124 and c1 == 124 { return true } # || if c0 == '>' and c1 == '>' { return true } # >>
if c0 == 46 and c1 == 46 { return true } # .. if c0 == '.' and c1 == '.' { return true } # ..
if c0 == 61 and c1 == 62 { return true } # => if c0 == '=' and c1 == '>' { return true } # =>
return false return false
} }
function is_op1(c: int) -> bool { function is_op1(c: int) -> bool {
return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59 if c == '+' or c == '-' or c == '*' or c == '/' or c == '%' or c == '<' or c == '>' or c == '=' { return true } # + - * / % < > =
if c == '(' or c == ')' or c == '{' or c == '}' or c == '[' or c == ']' { return true } # ( ) { } [ ]
if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ;
return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~
} }
function lex(s: pointer) -> void { function lex(s: pointer) -> void {
@ -137,45 +140,45 @@ program LudicFmt {
var i = 0; var line = 0 var i = 0; var line = 0
while s[i] != 0 { while s[i] != 0 {
let c = s[i] let c = s[i]
if c == 10 { push_tok(LT_NL, i, i + 1, line); i = i + 1; line = line + 1; push(linestart, i); continue } if c == '\n' { push_tok(LT_NL, i, i + 1, line); i += 1; line += 1; push(linestart, i); continue }
if c == 32 or c == 9 or c == 13 { i = i + 1; continue } if c == ' ' or c == '\t' or c == '\r' { i += 1; continue }
if c == 35 { # '#' comment to end of line if c == '#' { # '#' comment to end of line
let st = i; while s[i] != 0 and s[i] != 10 { i = i + 1 }; push_tok(LT_COMMENT, st, i, line); continue let st = i; while s[i] != 0 and s[i] != '\n' { i += 1 }; push_tok(LT_COMMENT, st, i, line); continue
} }
if c == 34 { # "string" if c == '"' { # "string"
let st = i; i = i + 1 let st = i; i += 1
while s[i] != 0 and s[i] != 34 and s[i] != 10 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } } while s[i] != 0 and s[i] != '"' and s[i] != '\n' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }
if s[i] == 34 { i = i + 1 } if s[i] == '"' { i += 1 }
push_tok(LT_STR, st, i, line); continue push_tok(LT_STR, st, i, line); continue
} }
if c == 96 { # `interpolated` if c == '`' { # `interpolated`
let st = i; i = i + 1 let st = i; i += 1
while s[i] != 0 and s[i] != 96 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } } while s[i] != 0 and s[i] != '`' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }
if s[i] == 96 { i = i + 1 } if s[i] == '`' { i += 1 }
push_tok(LT_STR, st, i, line); continue push_tok(LT_STR, st, i, line); continue
} }
if c == 39 { # 'c' if c == '\'' { # 'c'
let st = i; i = i + 1 let st = i; i += 1
if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { if s[i] != 0 and s[i] != 10 { i = i + 1 } } if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { if s[i] != 0 and s[i] != '\n' { i += 1 } }
if s[i] == 39 { i = i + 1 } if s[i] == '\'' { i += 1 }
push_tok(LT_CHAR, st, i, line); continue push_tok(LT_CHAR, st, i, line); continue
} }
if char_is_digit(c) { if char_is_digit(c) {
let st = i let st = i
if c == 48 and (s[i + 1] == 120 or s[i + 1] == 88) { # 0x hex if c == '0' and (s[i + 1] == 'x' or s[i + 1] == 'X') { # 0x hex
i = i + 2; while char_is_hex(s[i]) { i = i + 1 }; push_tok(LT_INT, st, i, line); continue i += 2; while char_is_hex(s[i]) { i += 1 }; push_tok(LT_INT, st, i, line); continue
} }
while char_is_digit(s[i]) { i = i + 1 } while char_is_digit(s[i]) { i += 1 }
if s[i] == 46 and char_is_digit(s[i + 1]) { if s[i] == '.' and char_is_digit(s[i + 1]) {
i = i + 1; while char_is_digit(s[i]) { i = i + 1 }; push_tok(LT_FLOAT, st, i, line); continue i += 1; while char_is_digit(s[i]) { i += 1 }; push_tok(LT_FLOAT, st, i, line); continue
} }
push_tok(LT_INT, st, i, line); continue push_tok(LT_INT, st, i, line); continue
} }
if c == 64 and (char_is_alpha(s[i + 1]) or s[i + 1] == 95) { # @name annotation if c == '@' and (char_is_alpha(s[i + 1]) or s[i + 1] == '_') { # @name annotation
let st = i; i = i + 1; while char_is_alnum(s[i]) { i = i + 1 }; push_tok(LT_ANNO, st, i, line); continue let st = i; i += 1; while char_is_alnum(s[i]) { i += 1 }; push_tok(LT_ANNO, st, i, line); continue
} }
if char_is_alpha(c) { if char_is_alpha(c) {
let st = i; while char_is_alnum(s[i]) { i = i + 1 } let st = i; while char_is_alnum(s[i]) { i += 1 }
let w = s[st..i] let w = s[st..i]
var k = LT_ID var k = LT_ID
if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL } if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL }
@ -184,14 +187,14 @@ program LudicFmt {
else { if is_keyword_word(w) { k = LT_KW } } } } else { if is_keyword_word(w) { k = LT_KW } } } }
push_tok(k, st, i, line); continue push_tok(k, st, i, line); continue
} }
if is_op2(c, s[i + 1]) { push_tok(LT_OP, i, i + 2, line); i = i + 2; continue } if is_op2(c, s[i + 1]) { push_tok(LT_OP, i, i + 2, line); i += 2; continue }
if is_op1(c) { push_tok(LT_OP, i, i + 1, line); i = i + 1; continue } if is_op1(c) { push_tok(LT_OP, i, i + 1, line); i += 1; continue }
# anything else: one UTF-8 character's worth as an LT_ERR token # anything else: one UTF-8 character's worth as an LT_ERR token
var ln = 1 var ln = 1
if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } } if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } }
var kk = 1 var kk = 1
while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk = kk + 1 } while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk += 1 }
push_tok(LT_ERR, i, i + ln, line); i = i + ln push_tok(LT_ERR, i, i + ln, line); i += ln
} }
push_tok(LT_EOF, i, i, line) push_tok(LT_EOF, i, i, line)
} }
@ -199,36 +202,41 @@ program LudicFmt {
# ---- token-stream helpers ---- # ---- token-stream helpers ----
function next_sig(i: int) -> int { function next_sig(i: int) -> int {
var j = i + 1 var j = i + 1
while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 } while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j += 1 }
return 0 - 1 return -1
} }
function prev_sig(i: int) -> int { function prev_sig(i: int) -> int {
var j = i - 1 var j = i - 1
while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 } while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j -= 1 }
return 0 - 1 return -1
} }
function name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL } function name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL }
# is token i written right after a '.', i.e. a member/method name?
function is_member(i: int) -> bool {
let p = prev_sig(i)
return p >= 0 and tk_kind[p] == LT_OP and tok_len(p) == 1 and src[tk_start[p]] == '.'
}
# a '.' hugs an operand, a closing bracket, and another dot # a '.' hugs an operand, a closing bracket, and another dot
function dot_tight(t: int) -> bool { function dot_tight(t: int) -> bool {
if name_like(tk_kind[t]) { return true } if name_like(tk_kind[t]) { return true }
if tk_kind[t] != LT_OP { return false } if tk_kind[t] != LT_OP { return false }
let n = tok_len(t); let c0 = src[tk_start[t]] let n = tok_len(t); let c0 = src[tk_start[t]]
if n == 1 and (c0 == 41 or c0 == 93 or c0 == 46) { return true } if n == 1 and (c0 == ')' or c0 == ']' or c0 == '.') { return true }
if n == 2 and c0 == 46 and src[tk_start[t] + 1] == 46 { return true } if n == 2 and c0 == '.' and src[tk_start[t] + 1] == 46 { return true }
return false return false
} }
# is the token at index i a unary '-'/'!' rather than a binary operator? # is the token at index i a unary '-' / '!' / '~' rather than a binary operator?
function is_unary(i: int) -> bool { function is_unary(i: int) -> bool {
if tk_kind[i] != LT_OP { return false } if tk_kind[i] != LT_OP { return false }
let n = tok_len(i) let n = tok_len(i)
if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33)) { return false } if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33 or src[tk_start[i]] == 126)) { return false }
let p = prev_sig(i) let p = prev_sig(i)
if p < 0 { return true } if p < 0 { return true }
let pk = tk_kind[p] let pk = tk_kind[p]
if pk == LT_ID or pk == LT_INT or pk == LT_FLOAT or pk == LT_STR or pk == LT_CHAR or pk == LT_BOOL or pk == LT_TYPE or pk == LT_PHASE { return false } if pk == LT_ID or pk == LT_INT or pk == LT_FLOAT or pk == LT_STR or pk == LT_CHAR or pk == LT_BOOL or pk == LT_TYPE or pk == LT_PHASE { return false }
if pk == LT_OP { if pk == LT_OP {
let c = src[tk_start[p]] let c = src[tk_start[p]]
return not (tok_len(p) == 1 and (c == 41 or c == 93 or c == 125)) # a closing bracket ends an operand return not (tok_len(p) == 1 and (c == ')' or c == ']' or c == '}')) # a closing bracket ends an operand
} }
return true # keyword/annotation/comment: operand starts here return true # keyword/annotation/comment: operand starts here
} }
@ -241,14 +249,26 @@ program LudicFmt {
let p0 = src[tk_start[prev]]; let c0 = src[tk_start[cur]] let p0 = src[tk_start[prev]]; let c0 = src[tk_start[cur]]
let p1 = (plen == 1); let c1 = (clen == 1) let p1 = (plen == 1); let c1 = (clen == 1)
if pk == LT_ERR or ck == LT_ERR { return tk_start[cur] - tk_end[prev] } # preserve an error token's spacing if pk == LT_ERR or ck == LT_ERR { return tk_start[cur] - tk_end[prev] } # preserve an error token's spacing
if c1 and (c0 == 41 or c0 == 93 or c0 == 44 or c0 == 58 or c0 == 59) { return 0 } # ) ] , : ; if c1 and (c0 == ')' or c0 == ']' or c0 == ',' or c0 == ':' or c0 == ';') { return 0 } # ) ] , : ;
if c1 and c0 == 46 and ck == LT_OP and dot_tight(prev) { return 0 } if c1 and c0 == '.' and ck == LT_OP and dot_tight(prev) { return 0 }
if p1 and p0 == 46 and pk == LT_OP and dot_tight(cur) { return 0 } if p1 and p0 == '.' and pk == LT_OP and dot_tight(cur) { return 0 }
if p1 and (p0 == 40 or p0 == 91) and pk == LT_OP { return 0 } # nothing hugs an opener from the right if p1 and (p0 == '(' or p0 == '[') and pk == LT_OP { return 0 } # nothing hugs an opener from the right
# an index or slice hugs its operand: a[i], f()[0], a[i][j]; `query [` and `= [` keep their space
if c1 and c0 == '[' and ck == LT_OP {
if pk == LT_ID or pk == LT_STR { return 0 }
if pk == LT_OP and p1 and (p0 == ')' or p0 == ']') { return 0 }
}
# the element type hugs an empty pair of brackets: `[]int`, `[]Node`
if p1 and p0 == ']' and pk == LT_OP and (ck == LT_ID or ck == LT_TYPE) {
let pp = prev_sig(prev)
if pp >= 0 and tk_kind[pp] == LT_OP and tok_len(pp) == 1 and src[tk_start[pp]] == 91 { return 0 }
}
if pk == LT_OP and is_unary(prev) { return 0 } if pk == LT_OP and is_unary(prev) { return 0 }
if pk == LT_ANNO and c1 and c0 == 40 { return 0 } # @anno( if pk == LT_ANNO and c1 and c0 == '(' { return 0 } # @anno(
if c1 and c0 == 40 and ck == LT_OP { if c1 and c0 == '(' and ck == LT_OP {
if pk == LT_ID or pk == LT_TYPE or pk == LT_PHASE { return 0 } # function move( / clear( if pk == LT_ID or pk == LT_TYPE or pk == LT_PHASE { return 0 } # function move( / clear(
if pk == LT_KW and (tok_text(prev) == "emit") { return 0 } # emit(...) is a call, `emit E(...)` an event
if pk == LT_KW and is_member(prev) { return 0 } # Prefab.spawn( / Date.new( — a member name is never a keyword
if pk == LT_OP { return 1 } # `= (`, `+ (`, `) (` keep a space if pk == LT_OP { return 1 } # `= (`, `+ (`, `) (` keep a space
return 1 return 1
} }
@ -264,7 +284,7 @@ program LudicFmt {
var cur = 0 var cur = 0
var open = 0 var open = 0
var brack = 0 var brack = 0
var ui_depth = 0 - 1 var ui_depth = -1
var pending_blank = 0 var pending_blank = 0
var wrote_any = 0 var wrote_any = 0
var prev_line_had_comment = 0 var prev_line_had_comment = 0
@ -272,9 +292,9 @@ program LudicFmt {
var i = 0 var i = 0
while i < N and tk_kind[i] != LT_EOF { while i < N and tk_kind[i] != LT_EOF {
let a = i let a = i
while i < N and tk_kind[i] != LT_NL and tk_kind[i] != LT_EOF { i = i + 1 } while i < N and tk_kind[i] != LT_NL and tk_kind[i] != LT_EOF { i += 1 }
let b = i let b = i
if i < N and tk_kind[i] == LT_NL { i = i + 1 } if i < N and tk_kind[i] == LT_NL { i += 1 }
if a == b { # a blank line if a == b { # a blank line
if wrote_any == 1 { pending_blank = 1 } if wrote_any == 1 { pending_blank = 1 }
@ -286,13 +306,13 @@ program LudicFmt {
var tsp = sp var tsp = sp
var t = a var t = a
while t < b and tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { # leading '}' while t < b and tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { # leading '}'
if tsp > 0 { tsp = tsp - 1; line_level = stack[tsp] } if tsp > 0 { tsp -= 1; line_level = stack[tsp] }
t = t + 1 t += 1
} }
# original column of this line # original column of this line
var orig_ind = 0 var orig_ind = 0
var kk = linestart[tk_line[a]] var kk = linestart[tk_line[a]]
while kk < tk_start[a] { if src[kk] == 9 { orig_ind = orig_ind + 4 } else { orig_ind = orig_ind + 1 }; kk = kk + 1 } while kk < tk_start[a] { if src[kk] == '\t' { orig_ind += 4 } else { orig_ind += 1 }; kk += 1 }
# a comment continuing the previous comment line keeps the author's column # a comment continuing the previous comment line keeps the author's column
var comment_run = 0 var comment_run = 0
if tk_kind[a] == LT_COMMENT and b == a + 1 and prev_line_had_comment == 1 { comment_run = 1 } if tk_kind[a] == LT_COMMENT and b == a + 1 and prev_line_had_comment == 1 { comment_run = 1 }
@ -304,7 +324,7 @@ program LudicFmt {
let ls = linestart[tk_line[a]] let ls = linestart[tk_line[a]]
ind = 0 ind = 0
var k2 = ls var k2 = ls
while k2 < tk_start[a] { if src[k2] == 9 { ind = ind + 4 } else { ind = ind + 1 }; k2 = k2 + 1 } while k2 < tk_start[a] { if src[k2] == '\t' { ind += 4 } else { ind += 1 }; k2 += 1 }
} else { } else {
var extra = 0 var extra = 0
if is_clause_word(tok_text(a)) and tk_kind[a] == LT_KW { extra = indent_width } if is_clause_word(tok_text(a)) and tk_kind[a] == LT_KW { extra = indent_width }
@ -312,13 +332,14 @@ program LudicFmt {
if comment_run == 1 and orig_ind > ind { ind = orig_ind } if comment_run == 1 and orig_ind > ind { ind = orig_ind }
} }
if pending_blank == 1 and wrote_any == 1 { buf_putc(o, 10) } if pending_blank == 1 and wrote_any == 1 { buf_putc(o, '\n') }
pending_blank = 0 pending_blank = 0
buf_indent(o, ind) buf_indent(o, ind)
# emit the tokens # emit the tokens
var prev = 0 - 1 var prev = -1
var line_brack = brack var line_brack = brack
var in_tag = 0 # inside a query's {Tag} filter
var line_ui_open = 0 var line_ui_open = 0
if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 } if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 }
t = a t = a
@ -327,9 +348,18 @@ program LudicFmt {
if tk_kind[t] == LT_COMMENT { if prev >= 0 { want = 2 } else { want = 0 } } if tk_kind[t] == LT_COMMENT { if prev >= 0 { want = 2 } else { want = 0 } }
else { else {
want = space_before(prev, t) want = space_before(prev, t)
if line_brack > 0 { # query {Tag} filter is one word # a query's {Tag} filter is one word: a `{` straight after `[` or `,` opens one
if tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { want = 0 } # (a record literal `new R { … }` inside a list keeps its spaces)
if prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and src[tk_start[prev]] == 123 { want = 0 } if line_brack > 0 and tk_kind[t] == LT_OP and tok_len(t) == 1 {
let c = src[tk_start[t]]
if c == '{' and prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and (src[tk_start[prev]] == '[' or src[tk_start[prev]] == ',') { in_tag = 1 }
if c == '}' and in_tag == 1 { want = 0; in_tag = 0 }
}
if in_tag == 1 and prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and src[tk_start[prev]] == '{' { want = 0 }
# a slice range hugs its bounds, `s[a..b]`; a `for i in a .. b` range keeps its spaces
if line_brack > 0 {
if tk_kind[t] == LT_OP and tok_len(t) == 2 and src[tk_start[t]] == 46 and src[tk_start[t] + 1] == 46 { want = 0 }
if prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 2 and src[tk_start[prev]] == 46 and src[tk_start[prev] + 1] == 46 { want = 0 }
} }
if line_ui_open == 1 { # widget props are k=v if line_ui_open == 1 { # widget props are k=v
var eq_here = 0 var eq_here = 0
@ -344,17 +374,17 @@ program LudicFmt {
if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap } # hand alignment wins if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap } # hand alignment wins
if want < 0 { want = 0 } if want < 0 { want = 0 }
var w2 = 0 var w2 = 0
while w2 < want { buf_putc(o, 32); w2 = w2 + 1 } while w2 < want { buf_putc(o, ' '); w2 += 1 }
buf_addrange(o, src, tk_start[t], tk_end[t]) buf_addrange(o, src, tk_start[t], tk_end[t])
prev = t prev = t
if tk_kind[t] == LT_OP and tok_len(t) == 1 { if tk_kind[t] == LT_OP and tok_len(t) == 1 {
let c = src[tk_start[t]] let c = src[tk_start[t]]
if c == 91 { line_brack = line_brack + 1 } if c == '[' { line_brack += 1 }
else { if c == 93 { line_brack = line_brack - 1; if line_brack < 0 { line_brack = 0 } } } else { if c == ']' { line_brack -= 1; if line_brack < 0 { line_brack = 0 } } }
} }
t = t + 1 t += 1
} }
buf_putc(o, 10) buf_putc(o, '\n')
wrote_any = 1 wrote_any = 1
prev_line_had_comment = 0 prev_line_had_comment = 0
if prev >= 0 and tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 } if prev >= 0 and tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 }
@ -366,25 +396,25 @@ program LudicFmt {
while t < b { while t < b {
if tk_kind[t] == LT_OP and tok_len(t) == 1 { if tk_kind[t] == LT_OP and tok_len(t) == 1 {
let c = src[tk_start[t]] let c = src[tk_start[t]]
if c == 123 { # '{' if c == '{' { # '{'
if sp < 512 { if sp < 512 {
let nxt = next_sig(t) let nxt = next_sig(t)
stack[sp] = line_level stack[sp] = line_level
var h = 0 var h = 0
if nxt >= 0 and nxt < b { h = 1 } if nxt >= 0 and nxt < b { h = 1 }
hang[sp] = h hang[sp] = h
sp = sp + 1 sp += 1
} }
level = line_level + 1 level = line_level + 1
} }
else { if c == 125 { if sp > 0 { sp = sp - 1; level = stack[sp] } } else { if c == '}' { if sp > 0 { sp -= 1; level = stack[sp] } }
else { if c == 40 or c == 91 { open = open + 1; if c == 91 { brack = brack + 1 } } else { if c == '(' or c == '[' { open += 1; if c == '[' { brack += 1 } }
else { if c == 41 or c == 93 { open = open - 1; if open < 0 { open = 0 }; if c == 93 { brack = brack - 1; if brack < 0 { brack = 0 } } } } } } else { if c == ')' or c == ']' { open -= 1; if open < 0 { open = 0 }; if c == ']' { brack -= 1; if brack < 0 { brack = 0 } } } } } }
} }
t = t + 1 t += 1
} }
cur = level cur = level
if ui_depth >= 0 and sp <= ui_depth { ui_depth = 0 - 1 } if ui_depth >= 0 and sp <= ui_depth { ui_depth = -1 }
} }
return o return o
} }
@ -395,22 +425,22 @@ program LudicFmt {
var g_marker: int = 0 var g_marker: int = 0
# detect a fence at line offset i; sets g_flen/g_info/g_marker; returns bool # detect a fence at line offset i; sets g_flen/g_info/g_marker; returns bool
function md_fence_at(s: pointer, i: int) -> bool { function md_fence_at(s: pointer, i: int) -> bool {
var j = i; while s[j] == 32 { j = j + 1 } var j = i; while s[j] == ' ' { j += 1 }
let m = s[j] let m = s[j]
if m != 96 and m != 126 { return false } # ` or ~ if m != '`' and m != '~' { return false } # ` or ~
var n = 0; while s[j] == m { j = j + 1; n = n + 1 } var n = 0; while s[j] == m { j += 1; n += 1 }
if n < 3 { return false } if n < 3 { return false }
g_flen = n; g_info = j; g_marker = m g_flen = n; g_info = j; g_marker = m
return true return true
} }
function md_info_is_ludic(s: pointer, at: int) -> bool { function md_info_is_ludic(s: pointer, at: int) -> bool {
var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 } var a = at; while s[a] == ' ' or s[a] == '\t' { a += 1 }
# case-insensitive "ludic" # case-insensitive "ludic"
if not (((s[a] == 108 or s[a] == 76)) and ((s[a + 1] == 117 or s[a + 1] == 85)) and ((s[a + 2] == 100 or s[a + 2] == 68)) and ((s[a + 3] == 105 or s[a + 3] == 73)) and ((s[a + 4] == 99 or s[a + 4] == 67))) { return false } if not (((s[a] == 'l' or s[a] == 'L')) and ((s[a + 1] == 'u' or s[a + 1] == 'U')) and ((s[a + 2] == 'd' or s[a + 2] == 'D')) and ((s[a + 3] == 'i' or s[a + 3] == 'I')) and ((s[a + 4] == 'c' or s[a + 4] == 'C'))) { return false }
let af = s[a + 5] let af = s[a + 5]
return af == 0 or af == 10 or af == 32 or af == 9 or af == 13 return af == 0 or af == '\n' or af == ' ' or af == '\t' or af == '\r'
} }
function line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e } function line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != '\n' { e += 1 }; return e }
function format_markdown(s: pointer, indent_width: int) -> Buf { function format_markdown(s: pointer, indent_width: int) -> Buf {
let o = buf_new() let o = buf_new()
@ -418,7 +448,7 @@ program LudicFmt {
while s[i] != 0 { while s[i] != 0 {
let ls = i let ls = i
let le = line_end(s, ls) let le = line_end(s, ls)
var indent = 0; while s[ls + indent] == 32 { indent = indent + 1 } var indent = 0; while s[ls + indent] == ' ' { indent += 1 }
if md_fence_at(s, ls) and md_info_is_ludic(s, g_info) { if md_fence_at(s, ls) and md_info_is_ludic(s, g_info) {
# copy the opening fence line verbatim (with its newline) # copy the opening fence line verbatim (with its newline)
var e0 = le; if s[le] != 0 { e0 = le + 1 } var e0 = le; if s[le] != 0 { e0 = le + 1 }
@ -437,7 +467,7 @@ program LudicFmt {
if g_marker == cmark and g_flen >= clen { if g_marker == cmark and g_flen >= clen {
var only = 1 var only = 1
var k = g_info var k = g_info
while k < pe { if s[k] != 32 and s[k] != 13 { only = 0; break }; k = k + 1 } while k < pe { if s[k] != ' ' and s[k] != '\r' { only = 0; break }; k += 1 }
if only == 1 { be = ps; break } if only == 1 { be = ps; break }
} }
} }
@ -450,9 +480,9 @@ program LudicFmt {
while p < be { while p < be {
var q = line_end(s, p) var q = line_end(s, p)
var skip = 0 var skip = 0
while skip < indent and (p + skip) < q and s[p + skip] == 32 { skip = skip + 1 } while skip < indent and (p + skip) < q and s[p + skip] == ' ' { skip += 1 }
buf_addrange(body, s, p + skip, q) buf_addrange(body, s, p + skip, q)
buf_putc(body, 10) buf_putc(body, '\n')
if s[q] != 0 { p = q + 1 } else { p = q } if s[q] != 0 { p = q + 1 } else { p = q }
} }
# feed the body through the ludic formatter # feed the body through the ludic formatter
@ -461,10 +491,10 @@ program LudicFmt {
let ftext = buf_str(f) let ftext = buf_str(f)
var fp = 0 var fp = 0
while ftext[fp] != 0 { while ftext[fp] != 0 {
var fq = fp; while ftext[fq] != 0 and ftext[fq] != 10 { fq = fq + 1 } var fq = fp; while ftext[fq] != 0 and ftext[fq] != '\n' { fq += 1 }
if fq > fp { buf_indent(o, indent) } if fq > fp { buf_indent(o, indent) }
buf_addrange(o, ftext, fp, fq) buf_addrange(o, ftext, fp, fq)
buf_putc(o, 10) buf_putc(o, '\n')
if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq } if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq }
} }
i = be i = be
@ -516,10 +546,10 @@ program LudicFmt {
if (a == "-w") or (a == "--write") { write = true } if (a == "-w") or (a == "--write") { write = true }
else { if (a == "--check") or (a == "-l") { check = true } else { if (a == "--check") or (a == "-l") { check = true }
else { if (a == "-q") or (a == "--quiet") { quiet = true } else { if (a == "-q") or (a == "--quiet") { quiet = true }
else { if (a == "--indent") { ai = ai + 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di = di + 1 } } } else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } }
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source"); return } else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source"); return }
else { push(files, a) } } } } } else { push(files, a) } } } } }
ai = ai + 1 ai += 1
} }
if indent < 1 or indent > 8 { indent = 2 } if indent < 1 or indent > 8 { indent = 2 }
@ -555,7 +585,7 @@ program LudicFmt {
file_write(file_stdout(), out, cstr_len(out)) file_write(file_stdout(), out, cstr_len(out))
} } } }
} }
fi = fi + 1 fi += 1
} }
if failed { exit(2) } if failed { exit(2) }
if check and changed { exit(1) } if check and changed { exit(1) }

File diff suppressed because it is too large Load diff

View file

@ -6,8 +6,8 @@
* addition to the language shows up in every editor at once instead of drifting * addition to the language shows up in every editor at once instead of drifting
* across six hand-maintained copies. * across six hand-maintained copies.
* *
* Unlike the compiler's lexer (compiler/ludicc.c) this one is written for * Unlike the compiler's lexer (selfhost/frontend/lex.ludic) this one is written
* editors: it keeps comments, keeps newlines, records byte spans for every * for editors: it keeps comments, keeps newlines, records byte spans for every
* token, and never exits on bad input — a stray character becomes an LT_ERR * token, and never exits on bad input — a stray character becomes an LT_ERR
* token and lexing continues, because a file being typed into is malformed most * token and lexing continues, because a file being typed into is malformed most
* of the time. * of the time.
@ -74,7 +74,7 @@ static const char* LUDIC_TYPES[] = {
"int","long","fixed","countdown","bool","entity","string","pointer","byte","words","fixeds","pointers","Vector","IVec2","Rect","void", 0 "int","long","fixed","countdown","bool","entity","string","pointer","byte","words","fixeds","pointers","Vector","IVec2","Rect","void", 0
}; };
static const char* LUDIC_PHASES[] = { static const char* LUDIC_PHASES[] = {
"Start","Input","FixedUpdate","Update","LateUpdate","Render", 0 "Start","Input","FixedUpdate","Update","LateUpdate","Render","Overlay", 0
}; };
static const char* LUDIC_WIDGETS[] = { static const char* LUDIC_WIDGETS[] = {
"panel","col","row","label","button","image","spacer", 0 "panel","col","row","label","button","image","spacer", 0
@ -208,7 +208,7 @@ static void lline_push(LLex* L, int off){
L->linestart[L->nline++] = off; L->linestart[L->nline++] = off;
} }
static const char* LUDIC_OPS2[] = { "->","+=","-=","*=","/=","==","!=","<=",">=","&&","||","..","=>", 0 }; static const char* LUDIC_OPS2[] = { "->","+=","-=","*=","/=","==","!=","<=",">=","<<",">>","..","=>", 0 };
/* Lex the whole buffer. Comments and newlines are kept — the formatter needs /* Lex the whole buffer. Comments and newlines are kept — the formatter needs
* both, and a highlighter needs the comments. */ * both, and a highlighter needs the comments. */
@ -279,7 +279,7 @@ static void lud_lex(LLex* L, const char* src){
if (matched) continue; if (matched) continue;
} }
if (c == ';'){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; } if (c == ';'){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; }
if (strchr("+-*/%<>=(){}[],:.!@", c)){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; } if (strchr("+-*/%<>=(){}[],:.!@&|^~", c)){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; }
/* Anything else is an error token — but a whole UTF-8 character's worth /* Anything else is an error token — but a whole UTF-8 character's worth
* at a time. Splitting a multi-byte character into one token per byte * at a time. Splitting a multi-byte character into one token per byte
* would let a consumer that re-emits tokens (the formatter) put spaces * would let a consumer that re-emits tokens (the formatter) put spaces