feat: add devtools (tree-sitter, neovim, formatter, CLI commands)
- Add tree-sitter grammar for syntax highlighting - Add Neovim plugin with syntax, LSP integration, and commands - Add code formatter (lux fmt) with check mode - Add CLI commands: fmt, check, test, watch, init - Add project initialization with lux.toml template Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
134
editors/nvim/README.md
Normal file
134
editors/nvim/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Lux Neovim Plugin
|
||||
|
||||
Neovim support for the Lux programming language.
|
||||
|
||||
## Features
|
||||
|
||||
- Syntax highlighting (vim regex and tree-sitter)
|
||||
- LSP integration (diagnostics, hover, completions, go-to-definition)
|
||||
- Commands for running, formatting, and testing
|
||||
- REPL integration
|
||||
|
||||
## Installation
|
||||
|
||||
### Using lazy.nvim
|
||||
|
||||
```lua
|
||||
{
|
||||
"your-org/lux",
|
||||
config = function()
|
||||
require("lux").setup({
|
||||
-- Optional: specify path to lux binary
|
||||
-- lux_binary = "/path/to/lux",
|
||||
lsp = {
|
||||
enabled = true,
|
||||
autostart = true,
|
||||
},
|
||||
format = {
|
||||
on_save = false,
|
||||
},
|
||||
})
|
||||
end,
|
||||
ft = "lux",
|
||||
}
|
||||
```
|
||||
|
||||
### Using packer.nvim
|
||||
|
||||
```lua
|
||||
use {
|
||||
"your-org/lux",
|
||||
config = function()
|
||||
require("lux").setup()
|
||||
end,
|
||||
ft = "lux",
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Installation
|
||||
|
||||
Copy the contents of this directory to your Neovim config:
|
||||
|
||||
```bash
|
||||
# Copy to nvim config
|
||||
cp -r editors/nvim/* ~/.config/nvim/
|
||||
|
||||
# Or symlink
|
||||
ln -s /path/to/lux/editors/nvim/ftdetect ~/.config/nvim/ftdetect
|
||||
ln -s /path/to/lux/editors/nvim/ftplugin ~/.config/nvim/ftplugin
|
||||
ln -s /path/to/lux/editors/nvim/syntax ~/.config/nvim/syntax
|
||||
ln -s /path/to/lux/editors/nvim/lua/lux ~/.config/nvim/lua/lux
|
||||
```
|
||||
|
||||
## Tree-sitter Support
|
||||
|
||||
For tree-sitter based highlighting, install the grammar:
|
||||
|
||||
```lua
|
||||
-- In your tree-sitter config
|
||||
require("nvim-treesitter.configs").setup({
|
||||
ensure_installed = { "lux" },
|
||||
highlight = { enable = true },
|
||||
})
|
||||
|
||||
-- Register the parser
|
||||
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
|
||||
parser_config.lux = {
|
||||
install_info = {
|
||||
url = "/path/to/lux/editors/tree-sitter-lux",
|
||||
files = { "src/parser.c" },
|
||||
},
|
||||
filetype = "lux",
|
||||
}
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:LuxRun` | Run the current file |
|
||||
| `:LuxFormat` | Format the current file |
|
||||
| `:LuxTest` | Run tests |
|
||||
| `:LuxRepl` | Start the REPL in a terminal |
|
||||
|
||||
## Key Mappings
|
||||
|
||||
Default mappings in Lux files (using `<localleader>`):
|
||||
|
||||
| Mapping | Action |
|
||||
|---------|--------|
|
||||
| `<localleader>r` | Run current file |
|
||||
| `<localleader>f` | Format current file |
|
||||
| `<localleader>t` | Run tests |
|
||||
|
||||
LSP mappings (when LSP is active):
|
||||
|
||||
| Mapping | Action |
|
||||
|---------|--------|
|
||||
| `gd` | Go to definition |
|
||||
| `K` | Hover information |
|
||||
| `gr` | Find references |
|
||||
| `<leader>rn` | Rename symbol |
|
||||
| `<leader>ca` | Code actions |
|
||||
| `[d` / `]d` | Previous/next diagnostic |
|
||||
|
||||
## Configuration
|
||||
|
||||
```lua
|
||||
require("lux").setup({
|
||||
-- Path to lux binary (searches PATH if not set)
|
||||
lux_binary = nil,
|
||||
|
||||
lsp = {
|
||||
-- Enable LSP support
|
||||
enabled = true,
|
||||
-- Auto-start LSP when opening .lux files
|
||||
autostart = true,
|
||||
},
|
||||
|
||||
format = {
|
||||
-- Format on save
|
||||
on_save = false,
|
||||
},
|
||||
})
|
||||
```
|
||||
141
editors/nvim/after/queries/lux/highlights.scm
Normal file
141
editors/nvim/after/queries/lux/highlights.scm
Normal file
@@ -0,0 +1,141 @@
|
||||
; Syntax highlighting queries for Lux
|
||||
|
||||
; Comments
|
||||
(line_comment) @comment
|
||||
(doc_comment) @comment.documentation
|
||||
|
||||
; Keywords
|
||||
[
|
||||
"fn"
|
||||
"let"
|
||||
"type"
|
||||
"effect"
|
||||
"handler"
|
||||
"trait"
|
||||
"impl"
|
||||
"for"
|
||||
"import"
|
||||
"export"
|
||||
"match"
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"with"
|
||||
"run"
|
||||
"resume"
|
||||
"is"
|
||||
] @keyword
|
||||
|
||||
; Operators
|
||||
[
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"%"
|
||||
"=="
|
||||
"!="
|
||||
"<"
|
||||
">"
|
||||
"<="
|
||||
">="
|
||||
"&&"
|
||||
"||"
|
||||
"!"
|
||||
"="
|
||||
"=>"
|
||||
"|>"
|
||||
":"
|
||||
"|"
|
||||
"."
|
||||
","
|
||||
] @operator
|
||||
|
||||
; Punctuation
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
"<"
|
||||
">"
|
||||
] @punctuation.bracket
|
||||
|
||||
; Literals
|
||||
(integer) @number
|
||||
(float) @number.float
|
||||
(string) @string
|
||||
(char) @character
|
||||
(boolean) @boolean
|
||||
(unit) @constant.builtin
|
||||
|
||||
; String interpolation
|
||||
(interpolation
|
||||
"{" @punctuation.special
|
||||
"}" @punctuation.special)
|
||||
|
||||
(escape_sequence) @string.escape
|
||||
|
||||
; Types
|
||||
(type_expression (identifier) @type)
|
||||
(generic_type (identifier) @type)
|
||||
(type_declaration name: (identifier) @type.definition)
|
||||
(type_parameters (identifier) @type.parameter)
|
||||
|
||||
; Built-in types
|
||||
((identifier) @type.builtin
|
||||
(#match? @type.builtin "^(Int|Float|Bool|String|Char|Unit|Option|Result|List)$"))
|
||||
|
||||
; Functions
|
||||
(function_declaration name: (identifier) @function.definition)
|
||||
(call_expression function: (identifier) @function.call)
|
||||
(call_expression function: (member_expression member: (identifier) @function.method))
|
||||
|
||||
; Effect operations
|
||||
(effect_declaration name: (identifier) @type.definition)
|
||||
(effect_operation name: (identifier) @function.definition)
|
||||
(handler_declaration name: (identifier) @function.definition)
|
||||
(handler_declaration effect: (identifier) @type)
|
||||
(handler_operation name: (identifier) @function.definition)
|
||||
|
||||
; Traits
|
||||
(trait_declaration name: (identifier) @type.definition)
|
||||
(trait_method name: (identifier) @function.definition)
|
||||
(impl_declaration trait: (identifier) @type)
|
||||
|
||||
; Parameters
|
||||
(parameter name: (identifier) @variable.parameter)
|
||||
|
||||
; Variables
|
||||
(let_declaration name: (identifier) @variable.definition)
|
||||
(identifier) @variable
|
||||
|
||||
; Patterns
|
||||
(pattern (identifier) @variable)
|
||||
(constructor_pattern name: (identifier) @constructor)
|
||||
(field_pattern name: (identifier) @property)
|
||||
|
||||
; Record fields
|
||||
(field_assignment name: (identifier) @property)
|
||||
(record_field name: (identifier) @property)
|
||||
|
||||
; Member access
|
||||
(member_expression member: (identifier) @property)
|
||||
|
||||
; Modules (capitalized identifiers used as module access)
|
||||
(member_expression
|
||||
object: (identifier) @module
|
||||
(#match? @module "^[A-Z]"))
|
||||
|
||||
; Variants/Constructors (capitalized identifiers)
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z][a-zA-Z0-9]*$"))
|
||||
|
||||
; Special identifiers
|
||||
((identifier) @constant.builtin
|
||||
(#match? @constant.builtin "^(None|Some|Ok|Err|true|false)$"))
|
||||
|
||||
; Property annotations
|
||||
(property_clause (identifier) @attribute)
|
||||
22
editors/nvim/after/queries/lux/indents.scm
Normal file
22
editors/nvim/after/queries/lux/indents.scm
Normal file
@@ -0,0 +1,22 @@
|
||||
; Indent rules for Lux
|
||||
|
||||
[
|
||||
(block_expression)
|
||||
(match_expression)
|
||||
(if_expression)
|
||||
(record_expression)
|
||||
(list_expression)
|
||||
(effect_declaration)
|
||||
(handler_declaration)
|
||||
(trait_declaration)
|
||||
(impl_declaration)
|
||||
(enum_definition)
|
||||
] @indent
|
||||
|
||||
[
|
||||
"}"
|
||||
"]"
|
||||
")"
|
||||
] @outdent
|
||||
|
||||
(match_arm "=>" @indent)
|
||||
21
editors/nvim/after/queries/lux/locals.scm
Normal file
21
editors/nvim/after/queries/lux/locals.scm
Normal file
@@ -0,0 +1,21 @@
|
||||
; Scopes
|
||||
(function_declaration) @scope
|
||||
(block_expression) @scope
|
||||
(lambda_expression) @scope
|
||||
(match_arm) @scope
|
||||
(handler_declaration) @scope
|
||||
|
||||
; Definitions
|
||||
(function_declaration name: (identifier) @definition.function)
|
||||
(let_declaration name: (identifier) @definition.var)
|
||||
(parameter name: (identifier) @definition.parameter)
|
||||
(type_declaration name: (identifier) @definition.type)
|
||||
(effect_declaration name: (identifier) @definition.type)
|
||||
(trait_declaration name: (identifier) @definition.type)
|
||||
(handler_declaration name: (identifier) @definition.function)
|
||||
|
||||
; Pattern bindings
|
||||
(pattern (identifier) @definition.var)
|
||||
|
||||
; References
|
||||
(identifier) @reference
|
||||
2
editors/nvim/ftdetect/lux.vim
Normal file
2
editors/nvim/ftdetect/lux.vim
Normal file
@@ -0,0 +1,2 @@
|
||||
" File type detection for Lux
|
||||
autocmd BufRead,BufNewFile *.lux setfiletype lux
|
||||
29
editors/nvim/ftplugin/lux.vim
Normal file
29
editors/nvim/ftplugin/lux.vim
Normal file
@@ -0,0 +1,29 @@
|
||||
" Lux filetype settings
|
||||
|
||||
" Indentation
|
||||
setlocal expandtab
|
||||
setlocal shiftwidth=4
|
||||
setlocal softtabstop=4
|
||||
setlocal tabstop=4
|
||||
|
||||
" Comments
|
||||
setlocal commentstring=//\ %s
|
||||
setlocal comments=://
|
||||
|
||||
" Folding
|
||||
setlocal foldmethod=syntax
|
||||
|
||||
" Match pairs
|
||||
setlocal matchpairs+=<:>
|
||||
|
||||
" Format options
|
||||
setlocal formatoptions-=t
|
||||
setlocal formatoptions+=croql
|
||||
|
||||
" Completion
|
||||
setlocal omnifunc=v:lua.vim.lsp.omnifunc
|
||||
|
||||
" Key mappings for Lux development
|
||||
nnoremap <buffer> <localleader>r :!lux %<CR>
|
||||
nnoremap <buffer> <localleader>f :!lux fmt %<CR>
|
||||
nnoremap <buffer> <localleader>t :!lux test<CR>
|
||||
163
editors/nvim/lua/lux/init.lua
Normal file
163
editors/nvim/lua/lux/init.lua
Normal file
@@ -0,0 +1,163 @@
|
||||
-- Lux language support for Neovim
|
||||
-- Provides LSP configuration and utilities
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Default configuration
|
||||
M.config = {
|
||||
-- Path to lux binary (will search PATH if not set)
|
||||
lux_binary = nil,
|
||||
-- Enable LSP
|
||||
lsp = {
|
||||
enabled = true,
|
||||
-- Auto-start LSP when opening .lux files
|
||||
autostart = true,
|
||||
},
|
||||
-- Formatting options
|
||||
format = {
|
||||
-- Format on save
|
||||
on_save = false,
|
||||
},
|
||||
}
|
||||
|
||||
-- Find lux binary
|
||||
local function find_lux_binary()
|
||||
if M.config.lux_binary then
|
||||
return M.config.lux_binary
|
||||
end
|
||||
-- Try to find in PATH
|
||||
local handle = io.popen("which lux 2>/dev/null")
|
||||
if handle then
|
||||
local result = handle:read("*a"):gsub("%s+", "")
|
||||
handle:close()
|
||||
if result ~= "" then
|
||||
return result
|
||||
end
|
||||
end
|
||||
-- Try common locations
|
||||
local common_paths = {
|
||||
"./result/bin/lux",
|
||||
"~/.local/bin/lux",
|
||||
"/usr/local/bin/lux",
|
||||
}
|
||||
for _, path in ipairs(common_paths) do
|
||||
local expanded = vim.fn.expand(path)
|
||||
if vim.fn.executable(expanded) == 1 then
|
||||
return expanded
|
||||
end
|
||||
end
|
||||
return "lux" -- Fallback, hope it's in PATH
|
||||
end
|
||||
|
||||
-- Setup LSP
|
||||
function M.setup_lsp()
|
||||
local lux_binary = find_lux_binary()
|
||||
|
||||
-- Check if lspconfig is available
|
||||
local ok, lspconfig = pcall(require, "lspconfig")
|
||||
if not ok then
|
||||
vim.notify("lspconfig not found. Install nvim-lspconfig for LSP support.", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local configs = require("lspconfig.configs")
|
||||
|
||||
-- Register lux LSP if not already registered
|
||||
if not configs.lux_lsp then
|
||||
configs.lux_lsp = {
|
||||
default_config = {
|
||||
cmd = { lux_binary, "lsp" },
|
||||
filetypes = { "lux" },
|
||||
root_dir = function(fname)
|
||||
return lspconfig.util.find_git_ancestor(fname)
|
||||
or lspconfig.util.root_pattern("lux.toml")(fname)
|
||||
or vim.fn.getcwd()
|
||||
end,
|
||||
settings = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- Setup the LSP
|
||||
lspconfig.lux_lsp.setup({
|
||||
on_attach = function(client, bufnr)
|
||||
-- Enable completion
|
||||
vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"
|
||||
|
||||
-- Key mappings
|
||||
local opts = { noremap = true, silent = true, buffer = bufnr }
|
||||
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
|
||||
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
|
||||
vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)
|
||||
vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts)
|
||||
vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
|
||||
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
|
||||
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
|
||||
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts)
|
||||
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
|
||||
|
||||
-- Format on save if enabled
|
||||
if M.config.format.on_save then
|
||||
vim.api.nvim_create_autocmd("BufWritePre", {
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
vim.lsp.buf.format({ async = false })
|
||||
end,
|
||||
})
|
||||
end
|
||||
end,
|
||||
capabilities = vim.lsp.protocol.make_client_capabilities(),
|
||||
})
|
||||
end
|
||||
|
||||
-- Run current file
|
||||
function M.run_file()
|
||||
local file = vim.fn.expand("%:p")
|
||||
local lux_binary = find_lux_binary()
|
||||
vim.cmd("!" .. lux_binary .. " " .. vim.fn.shellescape(file))
|
||||
end
|
||||
|
||||
-- Format current file
|
||||
function M.format_file()
|
||||
local file = vim.fn.expand("%:p")
|
||||
local lux_binary = find_lux_binary()
|
||||
vim.cmd("!" .. lux_binary .. " fmt " .. vim.fn.shellescape(file))
|
||||
vim.cmd("edit!") -- Reload the file
|
||||
end
|
||||
|
||||
-- Run tests
|
||||
function M.run_tests()
|
||||
local lux_binary = find_lux_binary()
|
||||
vim.cmd("!" .. lux_binary .. " test")
|
||||
end
|
||||
|
||||
-- Start REPL in terminal
|
||||
function M.start_repl()
|
||||
local lux_binary = find_lux_binary()
|
||||
vim.cmd("terminal " .. lux_binary)
|
||||
end
|
||||
|
||||
-- Setup function
|
||||
function M.setup(opts)
|
||||
-- Merge user config
|
||||
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
|
||||
|
||||
-- Setup LSP if enabled
|
||||
if M.config.lsp.enabled and M.config.lsp.autostart then
|
||||
vim.api.nvim_create_autocmd("FileType", {
|
||||
pattern = "lux",
|
||||
callback = function()
|
||||
M.setup_lsp()
|
||||
end,
|
||||
once = true,
|
||||
})
|
||||
end
|
||||
|
||||
-- Create user commands
|
||||
vim.api.nvim_create_user_command("LuxRun", M.run_file, { desc = "Run current Lux file" })
|
||||
vim.api.nvim_create_user_command("LuxFormat", M.format_file, { desc = "Format current Lux file" })
|
||||
vim.api.nvim_create_user_command("LuxTest", M.run_tests, { desc = "Run Lux tests" })
|
||||
vim.api.nvim_create_user_command("LuxRepl", M.start_repl, { desc = "Start Lux REPL" })
|
||||
end
|
||||
|
||||
return M
|
||||
89
editors/nvim/syntax/lux.vim
Normal file
89
editors/nvim/syntax/lux.vim
Normal file
@@ -0,0 +1,89 @@
|
||||
" Vim syntax file for Lux
|
||||
" Language: Lux
|
||||
" Maintainer: Lux Team
|
||||
|
||||
if exists("b:current_syntax")
|
||||
finish
|
||||
endif
|
||||
|
||||
" Keywords
|
||||
syn keyword luxKeyword fn let type effect handler trait impl for import export
|
||||
syn keyword luxKeyword match if then else with run resume is
|
||||
syn keyword luxKeyword nextgroup=luxFunction skipwhite
|
||||
|
||||
" Boolean literals
|
||||
syn keyword luxBoolean true false
|
||||
|
||||
" Built-in types
|
||||
syn keyword luxType Int Float Bool String Char Unit Option Result List
|
||||
syn keyword luxType Some None Ok Err
|
||||
|
||||
" Operators
|
||||
syn match luxOperator /[+\-*/%=<>!&|]/
|
||||
syn match luxOperator /==/
|
||||
syn match luxOperator /!=/
|
||||
syn match luxOperator /<=/
|
||||
syn match luxOperator />=/
|
||||
syn match luxOperator /&&/
|
||||
syn match luxOperator /||/
|
||||
syn match luxOperator /|>/
|
||||
syn match luxOperator /=>/
|
||||
|
||||
" Delimiters
|
||||
syn match luxDelimiter /[(){}\[\],;:]/
|
||||
syn match luxDelimiter /|/ contained
|
||||
|
||||
" Numbers
|
||||
syn match luxNumber /\<\d\+\>/
|
||||
syn match luxFloat /\<\d\+\.\d\+\>/
|
||||
|
||||
" Strings
|
||||
syn region luxString start=/"/ skip=/\\./ end=/"/ contains=luxEscape,luxInterpolation
|
||||
syn match luxEscape /\\[nrt\\'"0{}]/ contained
|
||||
syn region luxInterpolation start=/{/ end=/}/ contained contains=TOP
|
||||
|
||||
" Characters
|
||||
syn region luxChar start=/'/ skip=/\\./ end=/'/
|
||||
|
||||
" Comments
|
||||
syn match luxComment /\/\/.*$/
|
||||
syn match luxDocComment /\/\/\/.*$/
|
||||
|
||||
" Function definitions
|
||||
syn match luxFunction /\<[a-z_][a-zA-Z0-9_]*\>/ contained
|
||||
syn match luxFunctionCall /\<[a-z_][a-zA-Z0-9_]*\>\s*(/me=e-1
|
||||
|
||||
" Type definitions and constructors
|
||||
syn match luxTypedef /\<[A-Z][a-zA-Z0-9_]*\>/
|
||||
|
||||
" Module access
|
||||
syn match luxModule /\<[A-Z][a-zA-Z0-9_]*\>\./me=e-1
|
||||
|
||||
" Effect signatures
|
||||
syn region luxEffectSig start=/with\s*{/ end=/}/ contains=luxType,luxDelimiter
|
||||
|
||||
" Properties
|
||||
syn match luxProperty /\<is\s\+\(pure\|total\|idempotent\|commutative\|associative\)\>/
|
||||
|
||||
" Highlight groups
|
||||
hi def link luxKeyword Keyword
|
||||
hi def link luxBoolean Boolean
|
||||
hi def link luxType Type
|
||||
hi def link luxOperator Operator
|
||||
hi def link luxDelimiter Delimiter
|
||||
hi def link luxNumber Number
|
||||
hi def link luxFloat Float
|
||||
hi def link luxString String
|
||||
hi def link luxEscape SpecialChar
|
||||
hi def link luxInterpolation Special
|
||||
hi def link luxChar Character
|
||||
hi def link luxComment Comment
|
||||
hi def link luxDocComment SpecialComment
|
||||
hi def link luxFunction Function
|
||||
hi def link luxFunctionCall Function
|
||||
hi def link luxTypedef Type
|
||||
hi def link luxModule Include
|
||||
hi def link luxEffectSig Special
|
||||
hi def link luxProperty PreProc
|
||||
|
||||
let b:current_syntax = "lux"
|
||||
599
editors/tree-sitter-lux/grammar.js
Normal file
599
editors/tree-sitter-lux/grammar.js
Normal file
@@ -0,0 +1,599 @@
|
||||
// Tree-sitter grammar for the Lux programming language
|
||||
module.exports = grammar({
|
||||
name: 'lux',
|
||||
|
||||
extras: $ => [
|
||||
/\s/,
|
||||
$.line_comment,
|
||||
$.doc_comment,
|
||||
],
|
||||
|
||||
conflicts: $ => [
|
||||
[$.primary_expression, $.pattern],
|
||||
[$.type_expression, $.primary_expression],
|
||||
],
|
||||
|
||||
word: $ => $.identifier,
|
||||
|
||||
rules: {
|
||||
source_file: $ => repeat($._declaration),
|
||||
|
||||
_declaration: $ => choice(
|
||||
$.function_declaration,
|
||||
$.let_declaration,
|
||||
$.type_declaration,
|
||||
$.effect_declaration,
|
||||
$.handler_declaration,
|
||||
$.trait_declaration,
|
||||
$.impl_declaration,
|
||||
$.import_declaration,
|
||||
$.export_declaration,
|
||||
),
|
||||
|
||||
// Comments
|
||||
line_comment: $ => token(seq('//', /.*/)),
|
||||
doc_comment: $ => token(seq('///', /.*/)),
|
||||
|
||||
// Function declaration
|
||||
function_declaration: $ => seq(
|
||||
'fn',
|
||||
field('name', $.identifier),
|
||||
optional($.type_parameters),
|
||||
$.parameter_list,
|
||||
':',
|
||||
field('return_type', $.type_expression),
|
||||
optional($.effect_clause),
|
||||
optional($.property_clause),
|
||||
'=',
|
||||
field('body', $._expression),
|
||||
),
|
||||
|
||||
parameter_list: $ => seq(
|
||||
'(',
|
||||
optional(seq(
|
||||
$.parameter,
|
||||
repeat(seq(',', $.parameter)),
|
||||
optional(','),
|
||||
)),
|
||||
')',
|
||||
),
|
||||
|
||||
parameter: $ => seq(
|
||||
field('name', $.identifier),
|
||||
':',
|
||||
field('type', $.type_expression),
|
||||
),
|
||||
|
||||
type_parameters: $ => seq(
|
||||
'<',
|
||||
$.identifier,
|
||||
repeat(seq(',', $.identifier)),
|
||||
optional(','),
|
||||
'>',
|
||||
),
|
||||
|
||||
effect_clause: $ => seq(
|
||||
'with',
|
||||
'{',
|
||||
optional(seq(
|
||||
$.identifier,
|
||||
repeat(seq(',', $.identifier)),
|
||||
optional(','),
|
||||
)),
|
||||
'}',
|
||||
),
|
||||
|
||||
property_clause: $ => seq(
|
||||
'is',
|
||||
$.identifier,
|
||||
repeat(seq(',', $.identifier)),
|
||||
),
|
||||
|
||||
// Let declaration
|
||||
let_declaration: $ => seq(
|
||||
'let',
|
||||
field('name', $.identifier),
|
||||
optional(seq(':', field('type', $.type_expression))),
|
||||
'=',
|
||||
field('value', $._expression),
|
||||
),
|
||||
|
||||
// Type declaration
|
||||
type_declaration: $ => seq(
|
||||
'type',
|
||||
field('name', $.identifier),
|
||||
optional($.type_parameters),
|
||||
'=',
|
||||
field('definition', $.type_definition),
|
||||
),
|
||||
|
||||
type_definition: $ => choice(
|
||||
$.enum_definition,
|
||||
$.record_type,
|
||||
$.type_expression,
|
||||
),
|
||||
|
||||
enum_definition: $ => seq(
|
||||
repeat1(seq('|', $.variant)),
|
||||
),
|
||||
|
||||
variant: $ => seq(
|
||||
field('name', $.identifier),
|
||||
optional(choice(
|
||||
$.tuple_fields,
|
||||
$.record_fields,
|
||||
)),
|
||||
),
|
||||
|
||||
tuple_fields: $ => seq(
|
||||
'(',
|
||||
$.type_expression,
|
||||
repeat(seq(',', $.type_expression)),
|
||||
optional(','),
|
||||
')',
|
||||
),
|
||||
|
||||
record_fields: $ => seq(
|
||||
'{',
|
||||
$.record_field,
|
||||
repeat(seq(',', $.record_field)),
|
||||
optional(','),
|
||||
'}',
|
||||
),
|
||||
|
||||
record_field: $ => seq(
|
||||
field('name', $.identifier),
|
||||
':',
|
||||
field('type', $.type_expression),
|
||||
),
|
||||
|
||||
// Effect declaration
|
||||
effect_declaration: $ => seq(
|
||||
'effect',
|
||||
field('name', $.identifier),
|
||||
optional($.type_parameters),
|
||||
'{',
|
||||
repeat($.effect_operation),
|
||||
'}',
|
||||
),
|
||||
|
||||
effect_operation: $ => seq(
|
||||
'fn',
|
||||
field('name', $.identifier),
|
||||
$.parameter_list,
|
||||
':',
|
||||
field('return_type', $.type_expression),
|
||||
),
|
||||
|
||||
// Handler declaration
|
||||
handler_declaration: $ => seq(
|
||||
'handler',
|
||||
field('name', $.identifier),
|
||||
':',
|
||||
field('effect', $.identifier),
|
||||
'{',
|
||||
repeat($.handler_operation),
|
||||
'}',
|
||||
),
|
||||
|
||||
handler_operation: $ => seq(
|
||||
'fn',
|
||||
field('name', $.identifier),
|
||||
'(',
|
||||
optional(seq(
|
||||
$.identifier,
|
||||
repeat(seq(',', $.identifier)),
|
||||
optional(','),
|
||||
)),
|
||||
')',
|
||||
'=',
|
||||
field('body', $._expression),
|
||||
),
|
||||
|
||||
// Trait declaration
|
||||
trait_declaration: $ => seq(
|
||||
'trait',
|
||||
field('name', $.identifier),
|
||||
optional($.type_parameters),
|
||||
'{',
|
||||
repeat($.trait_method),
|
||||
'}',
|
||||
),
|
||||
|
||||
trait_method: $ => seq(
|
||||
'fn',
|
||||
field('name', $.identifier),
|
||||
$.parameter_list,
|
||||
':',
|
||||
field('return_type', $.type_expression),
|
||||
),
|
||||
|
||||
// Impl declaration
|
||||
impl_declaration: $ => seq(
|
||||
'impl',
|
||||
field('trait', $.identifier),
|
||||
'for',
|
||||
field('type', $.type_expression),
|
||||
'{',
|
||||
repeat($.function_declaration),
|
||||
'}',
|
||||
),
|
||||
|
||||
// Import/Export
|
||||
import_declaration: $ => seq(
|
||||
'import',
|
||||
$.import_path,
|
||||
optional($.import_clause),
|
||||
),
|
||||
|
||||
import_path: $ => seq(
|
||||
$.identifier,
|
||||
repeat(seq('.', $.identifier)),
|
||||
),
|
||||
|
||||
import_clause: $ => seq(
|
||||
'{',
|
||||
$.identifier,
|
||||
repeat(seq(',', $.identifier)),
|
||||
optional(','),
|
||||
'}',
|
||||
),
|
||||
|
||||
export_declaration: $ => seq(
|
||||
'export',
|
||||
'{',
|
||||
$.identifier,
|
||||
repeat(seq(',', $.identifier)),
|
||||
optional(','),
|
||||
'}',
|
||||
),
|
||||
|
||||
// Type expressions
|
||||
type_expression: $ => choice(
|
||||
$.identifier,
|
||||
$.generic_type,
|
||||
$.function_type,
|
||||
$.tuple_type,
|
||||
$.record_type,
|
||||
seq('(', $.type_expression, ')'),
|
||||
),
|
||||
|
||||
generic_type: $ => seq(
|
||||
$.identifier,
|
||||
'<',
|
||||
$.type_expression,
|
||||
repeat(seq(',', $.type_expression)),
|
||||
optional(','),
|
||||
'>',
|
||||
),
|
||||
|
||||
function_type: $ => seq(
|
||||
'fn',
|
||||
'(',
|
||||
optional(seq(
|
||||
$.type_expression,
|
||||
repeat(seq(',', $.type_expression)),
|
||||
optional(','),
|
||||
)),
|
||||
')',
|
||||
':',
|
||||
$.type_expression,
|
||||
optional($.effect_clause),
|
||||
),
|
||||
|
||||
tuple_type: $ => seq(
|
||||
'(',
|
||||
$.type_expression,
|
||||
',',
|
||||
$.type_expression,
|
||||
repeat(seq(',', $.type_expression)),
|
||||
optional(','),
|
||||
')',
|
||||
),
|
||||
|
||||
record_type: $ => seq(
|
||||
'{',
|
||||
optional(seq(
|
||||
$.record_field,
|
||||
repeat(seq(',', $.record_field)),
|
||||
optional(','),
|
||||
)),
|
||||
'}',
|
||||
),
|
||||
|
||||
// Expressions
|
||||
_expression: $ => choice(
|
||||
$.primary_expression,
|
||||
$.binary_expression,
|
||||
$.unary_expression,
|
||||
$.call_expression,
|
||||
$.member_expression,
|
||||
$.index_expression,
|
||||
$.if_expression,
|
||||
$.match_expression,
|
||||
$.block_expression,
|
||||
$.lambda_expression,
|
||||
$.run_expression,
|
||||
$.resume_expression,
|
||||
$.pipe_expression,
|
||||
),
|
||||
|
||||
primary_expression: $ => choice(
|
||||
$.identifier,
|
||||
$.number,
|
||||
$.string,
|
||||
$.char,
|
||||
$.boolean,
|
||||
$.unit,
|
||||
$.list_expression,
|
||||
$.tuple_expression,
|
||||
$.record_expression,
|
||||
seq('(', $._expression, ')'),
|
||||
),
|
||||
|
||||
// Literals
|
||||
number: $ => choice(
|
||||
$.integer,
|
||||
$.float,
|
||||
),
|
||||
|
||||
integer: $ => /\d+/,
|
||||
|
||||
float: $ => /\d+\.\d+/,
|
||||
|
||||
string: $ => choice(
|
||||
$.simple_string,
|
||||
$.interpolated_string,
|
||||
),
|
||||
|
||||
simple_string: $ => seq(
|
||||
'"',
|
||||
repeat(choice(
|
||||
$.string_content,
|
||||
$.escape_sequence,
|
||||
)),
|
||||
'"',
|
||||
),
|
||||
|
||||
interpolated_string: $ => seq(
|
||||
'"',
|
||||
repeat(choice(
|
||||
$.string_content,
|
||||
$.escape_sequence,
|
||||
$.interpolation,
|
||||
)),
|
||||
'"',
|
||||
),
|
||||
|
||||
string_content: $ => /[^"\\{]+/,
|
||||
|
||||
escape_sequence: $ => /\\[nrt\\'"0{}\}]/,
|
||||
|
||||
interpolation: $ => seq(
|
||||
'{',
|
||||
$._expression,
|
||||
'}',
|
||||
),
|
||||
|
||||
char: $ => seq(
|
||||
"'",
|
||||
choice(
|
||||
/[^'\\]/,
|
||||
$.escape_sequence,
|
||||
),
|
||||
"'",
|
||||
),
|
||||
|
||||
boolean: $ => choice('true', 'false'),
|
||||
|
||||
unit: $ => seq('(', ')'),
|
||||
|
||||
list_expression: $ => seq(
|
||||
'[',
|
||||
optional(seq(
|
||||
$._expression,
|
||||
repeat(seq(',', $._expression)),
|
||||
optional(','),
|
||||
)),
|
||||
']',
|
||||
),
|
||||
|
||||
tuple_expression: $ => seq(
|
||||
'(',
|
||||
$._expression,
|
||||
',',
|
||||
$._expression,
|
||||
repeat(seq(',', $._expression)),
|
||||
optional(','),
|
||||
')',
|
||||
),
|
||||
|
||||
record_expression: $ => seq(
|
||||
'{',
|
||||
optional(seq(
|
||||
$.field_assignment,
|
||||
repeat(seq(',', $.field_assignment)),
|
||||
optional(','),
|
||||
)),
|
||||
'}',
|
||||
),
|
||||
|
||||
field_assignment: $ => seq(
|
||||
field('name', $.identifier),
|
||||
':',
|
||||
field('value', $._expression),
|
||||
),
|
||||
|
||||
// Binary expressions
|
||||
binary_expression: $ => choice(
|
||||
prec.left(1, seq($._expression, '||', $._expression)),
|
||||
prec.left(2, seq($._expression, '&&', $._expression)),
|
||||
prec.left(3, seq($._expression, choice('==', '!='), $._expression)),
|
||||
prec.left(4, seq($._expression, choice('<', '>', '<=', '>='), $._expression)),
|
||||
prec.left(5, seq($._expression, choice('+', '-'), $._expression)),
|
||||
prec.left(6, seq($._expression, choice('*', '/', '%'), $._expression)),
|
||||
),
|
||||
|
||||
unary_expression: $ => prec(7, choice(
|
||||
seq('-', $._expression),
|
||||
seq('!', $._expression),
|
||||
)),
|
||||
|
||||
// Pipe expression
|
||||
pipe_expression: $ => prec.left(0, seq(
|
||||
$._expression,
|
||||
'|>',
|
||||
$._expression,
|
||||
)),
|
||||
|
||||
// Call expression
|
||||
call_expression: $ => prec(8, seq(
|
||||
field('function', $._expression),
|
||||
'(',
|
||||
optional(seq(
|
||||
$._expression,
|
||||
repeat(seq(',', $._expression)),
|
||||
optional(','),
|
||||
)),
|
||||
')',
|
||||
)),
|
||||
|
||||
// Member expression
|
||||
member_expression: $ => prec(9, seq(
|
||||
field('object', $._expression),
|
||||
'.',
|
||||
field('member', $.identifier),
|
||||
)),
|
||||
|
||||
// Index expression
|
||||
index_expression: $ => prec(8, seq(
|
||||
field('object', $._expression),
|
||||
'[',
|
||||
field('index', $._expression),
|
||||
']',
|
||||
)),
|
||||
|
||||
// If expression
|
||||
if_expression: $ => prec.right(seq(
|
||||
'if',
|
||||
field('condition', $._expression),
|
||||
'then',
|
||||
field('then', $._expression),
|
||||
'else',
|
||||
field('else', $._expression),
|
||||
)),
|
||||
|
||||
// Match expression
|
||||
match_expression: $ => seq(
|
||||
'match',
|
||||
field('value', $._expression),
|
||||
'{',
|
||||
repeat1($.match_arm),
|
||||
'}',
|
||||
),
|
||||
|
||||
match_arm: $ => seq(
|
||||
field('pattern', $.pattern),
|
||||
'=>',
|
||||
field('body', $._expression),
|
||||
optional(','),
|
||||
),
|
||||
|
||||
pattern: $ => choice(
|
||||
$.identifier,
|
||||
$.number,
|
||||
$.string,
|
||||
$.boolean,
|
||||
'_',
|
||||
$.constructor_pattern,
|
||||
$.tuple_pattern,
|
||||
$.record_pattern,
|
||||
),
|
||||
|
||||
constructor_pattern: $ => seq(
|
||||
field('name', $.identifier),
|
||||
optional(seq(
|
||||
'(',
|
||||
$.pattern,
|
||||
repeat(seq(',', $.pattern)),
|
||||
optional(','),
|
||||
')',
|
||||
)),
|
||||
),
|
||||
|
||||
tuple_pattern: $ => seq(
|
||||
'(',
|
||||
$.pattern,
|
||||
',',
|
||||
$.pattern,
|
||||
repeat(seq(',', $.pattern)),
|
||||
optional(','),
|
||||
')',
|
||||
),
|
||||
|
||||
record_pattern: $ => seq(
|
||||
'{',
|
||||
$.field_pattern,
|
||||
repeat(seq(',', $.field_pattern)),
|
||||
optional(','),
|
||||
'}',
|
||||
),
|
||||
|
||||
field_pattern: $ => seq(
|
||||
field('name', $.identifier),
|
||||
optional(seq(':', field('pattern', $.pattern))),
|
||||
),
|
||||
|
||||
// Block expression
|
||||
block_expression: $ => seq(
|
||||
'{',
|
||||
repeat($.statement),
|
||||
optional($._expression),
|
||||
'}',
|
||||
),
|
||||
|
||||
statement: $ => seq(
|
||||
$.let_declaration,
|
||||
),
|
||||
|
||||
// Lambda expression
|
||||
lambda_expression: $ => seq(
|
||||
'fn',
|
||||
$.parameter_list,
|
||||
optional(seq(':', $.type_expression)),
|
||||
'=>',
|
||||
$._expression,
|
||||
),
|
||||
|
||||
// Run expression
|
||||
run_expression: $ => seq(
|
||||
'run',
|
||||
field('expression', $._expression),
|
||||
'with',
|
||||
'{',
|
||||
optional(seq(
|
||||
$.handler_binding,
|
||||
repeat(seq(',', $.handler_binding)),
|
||||
optional(','),
|
||||
)),
|
||||
'}',
|
||||
),
|
||||
|
||||
handler_binding: $ => seq(
|
||||
field('effect', $.identifier),
|
||||
'=',
|
||||
field('handler', $.identifier),
|
||||
),
|
||||
|
||||
// Resume expression
|
||||
resume_expression: $ => seq(
|
||||
'resume',
|
||||
'(',
|
||||
$._expression,
|
||||
')',
|
||||
),
|
||||
|
||||
// Identifier
|
||||
identifier: $ => /[a-zA-Z_][a-zA-Z0-9_]*/,
|
||||
}
|
||||
});
|
||||
38
editors/tree-sitter-lux/package.json
Normal file
38
editors/tree-sitter-lux/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "tree-sitter-lux",
|
||||
"version": "0.1.0",
|
||||
"description": "Tree-sitter grammar for the Lux programming language",
|
||||
"main": "bindings/node",
|
||||
"keywords": [
|
||||
"tree-sitter",
|
||||
"parser",
|
||||
"lux"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/your-org/lux"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nan": "^2.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tree-sitter-cli": "^0.20.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tree-sitter generate && node-gyp rebuild",
|
||||
"test": "tree-sitter test",
|
||||
"parse": "tree-sitter parse"
|
||||
},
|
||||
"tree-sitter": [
|
||||
{
|
||||
"scope": "source.lux",
|
||||
"injection-regex": "lux",
|
||||
"file-types": [
|
||||
"lux"
|
||||
],
|
||||
"highlights": "queries/highlights.scm",
|
||||
"locals": "queries/locals.scm"
|
||||
}
|
||||
]
|
||||
}
|
||||
141
editors/tree-sitter-lux/queries/highlights.scm
Normal file
141
editors/tree-sitter-lux/queries/highlights.scm
Normal file
@@ -0,0 +1,141 @@
|
||||
; Syntax highlighting queries for Lux
|
||||
|
||||
; Comments
|
||||
(line_comment) @comment
|
||||
(doc_comment) @comment.documentation
|
||||
|
||||
; Keywords
|
||||
[
|
||||
"fn"
|
||||
"let"
|
||||
"type"
|
||||
"effect"
|
||||
"handler"
|
||||
"trait"
|
||||
"impl"
|
||||
"for"
|
||||
"import"
|
||||
"export"
|
||||
"match"
|
||||
"if"
|
||||
"then"
|
||||
"else"
|
||||
"with"
|
||||
"run"
|
||||
"resume"
|
||||
"is"
|
||||
] @keyword
|
||||
|
||||
; Operators
|
||||
[
|
||||
"+"
|
||||
"-"
|
||||
"*"
|
||||
"/"
|
||||
"%"
|
||||
"=="
|
||||
"!="
|
||||
"<"
|
||||
">"
|
||||
"<="
|
||||
">="
|
||||
"&&"
|
||||
"||"
|
||||
"!"
|
||||
"="
|
||||
"=>"
|
||||
"|>"
|
||||
":"
|
||||
"|"
|
||||
"."
|
||||
","
|
||||
] @operator
|
||||
|
||||
; Punctuation
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
"<"
|
||||
">"
|
||||
] @punctuation.bracket
|
||||
|
||||
; Literals
|
||||
(integer) @number
|
||||
(float) @number.float
|
||||
(string) @string
|
||||
(char) @character
|
||||
(boolean) @boolean
|
||||
(unit) @constant.builtin
|
||||
|
||||
; String interpolation
|
||||
(interpolation
|
||||
"{" @punctuation.special
|
||||
"}" @punctuation.special)
|
||||
|
||||
(escape_sequence) @string.escape
|
||||
|
||||
; Types
|
||||
(type_expression (identifier) @type)
|
||||
(generic_type (identifier) @type)
|
||||
(type_declaration name: (identifier) @type.definition)
|
||||
(type_parameters (identifier) @type.parameter)
|
||||
|
||||
; Built-in types
|
||||
((identifier) @type.builtin
|
||||
(#match? @type.builtin "^(Int|Float|Bool|String|Char|Unit|Option|Result|List)$"))
|
||||
|
||||
; Functions
|
||||
(function_declaration name: (identifier) @function.definition)
|
||||
(call_expression function: (identifier) @function.call)
|
||||
(call_expression function: (member_expression member: (identifier) @function.method))
|
||||
|
||||
; Effect operations
|
||||
(effect_declaration name: (identifier) @type.definition)
|
||||
(effect_operation name: (identifier) @function.definition)
|
||||
(handler_declaration name: (identifier) @function.definition)
|
||||
(handler_declaration effect: (identifier) @type)
|
||||
(handler_operation name: (identifier) @function.definition)
|
||||
|
||||
; Traits
|
||||
(trait_declaration name: (identifier) @type.definition)
|
||||
(trait_method name: (identifier) @function.definition)
|
||||
(impl_declaration trait: (identifier) @type)
|
||||
|
||||
; Parameters
|
||||
(parameter name: (identifier) @variable.parameter)
|
||||
|
||||
; Variables
|
||||
(let_declaration name: (identifier) @variable.definition)
|
||||
(identifier) @variable
|
||||
|
||||
; Patterns
|
||||
(pattern (identifier) @variable)
|
||||
(constructor_pattern name: (identifier) @constructor)
|
||||
(field_pattern name: (identifier) @property)
|
||||
|
||||
; Record fields
|
||||
(field_assignment name: (identifier) @property)
|
||||
(record_field name: (identifier) @property)
|
||||
|
||||
; Member access
|
||||
(member_expression member: (identifier) @property)
|
||||
|
||||
; Modules (capitalized identifiers used as module access)
|
||||
(member_expression
|
||||
object: (identifier) @module
|
||||
(#match? @module "^[A-Z]"))
|
||||
|
||||
; Variants/Constructors (capitalized identifiers)
|
||||
((identifier) @constructor
|
||||
(#match? @constructor "^[A-Z][a-zA-Z0-9]*$"))
|
||||
|
||||
; Special identifiers
|
||||
((identifier) @constant.builtin
|
||||
(#match? @constant.builtin "^(None|Some|Ok|Err|true|false)$"))
|
||||
|
||||
; Property annotations
|
||||
(property_clause (identifier) @attribute)
|
||||
22
editors/tree-sitter-lux/queries/indents.scm
Normal file
22
editors/tree-sitter-lux/queries/indents.scm
Normal file
@@ -0,0 +1,22 @@
|
||||
; Indent rules for Lux
|
||||
|
||||
[
|
||||
(block_expression)
|
||||
(match_expression)
|
||||
(if_expression)
|
||||
(record_expression)
|
||||
(list_expression)
|
||||
(effect_declaration)
|
||||
(handler_declaration)
|
||||
(trait_declaration)
|
||||
(impl_declaration)
|
||||
(enum_definition)
|
||||
] @indent
|
||||
|
||||
[
|
||||
"}"
|
||||
"]"
|
||||
")"
|
||||
] @outdent
|
||||
|
||||
(match_arm "=>" @indent)
|
||||
21
editors/tree-sitter-lux/queries/locals.scm
Normal file
21
editors/tree-sitter-lux/queries/locals.scm
Normal file
@@ -0,0 +1,21 @@
|
||||
; Scopes
|
||||
(function_declaration) @scope
|
||||
(block_expression) @scope
|
||||
(lambda_expression) @scope
|
||||
(match_arm) @scope
|
||||
(handler_declaration) @scope
|
||||
|
||||
; Definitions
|
||||
(function_declaration name: (identifier) @definition.function)
|
||||
(let_declaration name: (identifier) @definition.var)
|
||||
(parameter name: (identifier) @definition.parameter)
|
||||
(type_declaration name: (identifier) @definition.type)
|
||||
(effect_declaration name: (identifier) @definition.type)
|
||||
(trait_declaration name: (identifier) @definition.type)
|
||||
(handler_declaration name: (identifier) @definition.function)
|
||||
|
||||
; Pattern bindings
|
||||
(pattern (identifier) @definition.var)
|
||||
|
||||
; References
|
||||
(identifier) @reference
|
||||
790
src/formatter.rs
Normal file
790
src/formatter.rs
Normal file
@@ -0,0 +1,790 @@
|
||||
//! Code formatter for Lux
|
||||
//!
|
||||
//! Formats Lux source code according to standard style guidelines.
|
||||
|
||||
use crate::ast::{
|
||||
BehavioralProperty, BinaryOp, Declaration, EffectDecl, Expr, FunctionDecl, HandlerDecl,
|
||||
ImplDecl, ImplMethod, LetDecl, Literal, LiteralKind, Pattern, Program, Statement, TraitDecl,
|
||||
TypeDecl, TypeDef, TypeExpr, UnaryOp, VariantFields,
|
||||
};
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
|
||||
/// Formatter configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FormatConfig {
|
||||
/// Number of spaces for indentation
|
||||
pub indent_size: usize,
|
||||
/// Maximum line width before wrapping
|
||||
pub max_width: usize,
|
||||
/// Add trailing commas in multi-line constructs
|
||||
pub trailing_commas: bool,
|
||||
}
|
||||
|
||||
impl Default for FormatConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indent_size: 4,
|
||||
max_width: 100,
|
||||
trailing_commas: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format Lux source code
|
||||
pub fn format(source: &str, config: &FormatConfig) -> Result<String, String> {
|
||||
// Parse the source
|
||||
let lexer = Lexer::new(source);
|
||||
let tokens = lexer.tokenize().map_err(|e| e.message)?;
|
||||
let mut parser = Parser::new(tokens);
|
||||
let program = parser.parse_program().map_err(|e| e.message)?;
|
||||
|
||||
// Format the AST
|
||||
let mut formatter = Formatter::new(config.clone());
|
||||
Ok(formatter.format_program(&program))
|
||||
}
|
||||
|
||||
/// Formatter state
|
||||
struct Formatter {
|
||||
config: FormatConfig,
|
||||
output: String,
|
||||
indent_level: usize,
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
fn new(config: FormatConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
output: String::new(),
|
||||
indent_level: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn indent(&self) -> String {
|
||||
" ".repeat(self.indent_level * self.config.indent_size)
|
||||
}
|
||||
|
||||
fn write(&mut self, s: &str) {
|
||||
self.output.push_str(s);
|
||||
}
|
||||
|
||||
fn writeln(&mut self, s: &str) {
|
||||
self.output.push_str(s);
|
||||
self.output.push('\n');
|
||||
}
|
||||
|
||||
fn newline(&mut self) {
|
||||
self.output.push('\n');
|
||||
}
|
||||
|
||||
fn format_program(&mut self, program: &Program) -> String {
|
||||
let mut first = true;
|
||||
for decl in &program.declarations {
|
||||
if !first {
|
||||
self.newline();
|
||||
}
|
||||
first = false;
|
||||
self.format_declaration(decl);
|
||||
}
|
||||
// Ensure file ends with newline
|
||||
if !self.output.ends_with('\n') {
|
||||
self.newline();
|
||||
}
|
||||
self.output.clone()
|
||||
}
|
||||
|
||||
fn format_declaration(&mut self, decl: &Declaration) {
|
||||
match decl {
|
||||
Declaration::Function(f) => self.format_function(f),
|
||||
Declaration::Let(l) => self.format_let(l),
|
||||
Declaration::Type(t) => self.format_type_decl(t),
|
||||
Declaration::Effect(e) => self.format_effect(e),
|
||||
Declaration::Handler(h) => self.format_handler(h),
|
||||
Declaration::Trait(t) => self.format_trait(t),
|
||||
Declaration::Impl(i) => self.format_impl(i),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_function(&mut self, func: &FunctionDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("fn ");
|
||||
self.write(&func.name.name);
|
||||
|
||||
// Type parameters
|
||||
if !func.type_params.is_empty() {
|
||||
self.write("<");
|
||||
self.write(
|
||||
&func
|
||||
.type_params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(">");
|
||||
}
|
||||
|
||||
// Parameters
|
||||
self.write("(");
|
||||
let params: Vec<String> = func
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
self.write(¶ms.join(", "));
|
||||
self.write("): ");
|
||||
|
||||
// Return type
|
||||
self.write(&self.format_type_expr(&func.return_type));
|
||||
|
||||
// Effects
|
||||
if !func.effects.is_empty() {
|
||||
self.write(" with {");
|
||||
self.write(
|
||||
&func
|
||||
.effects
|
||||
.iter()
|
||||
.map(|e| e.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write("}");
|
||||
}
|
||||
|
||||
// Properties
|
||||
if !func.properties.is_empty() {
|
||||
self.write(" is ");
|
||||
self.write(
|
||||
&func
|
||||
.properties
|
||||
.iter()
|
||||
.map(|p| self.format_property(p))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
}
|
||||
|
||||
self.write(" =");
|
||||
|
||||
// Body
|
||||
let body_str = self.format_expr(&func.body);
|
||||
if self.is_block_expr(&func.body) || body_str.contains('\n') {
|
||||
self.newline();
|
||||
self.indent_level += 1;
|
||||
self.write(&self.indent());
|
||||
self.write(&body_str);
|
||||
self.indent_level -= 1;
|
||||
} else {
|
||||
self.write(" ");
|
||||
self.write(&body_str);
|
||||
}
|
||||
self.newline();
|
||||
}
|
||||
|
||||
fn format_let(&mut self, let_decl: &LetDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("let ");
|
||||
self.write(&let_decl.name.name);
|
||||
|
||||
if let Some(ref typ) = let_decl.typ {
|
||||
self.write(": ");
|
||||
self.write(&self.format_type_expr(typ));
|
||||
}
|
||||
|
||||
self.write(" = ");
|
||||
self.write(&self.format_expr(&let_decl.value));
|
||||
self.newline();
|
||||
}
|
||||
|
||||
fn format_type_decl(&mut self, type_decl: &TypeDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("type ");
|
||||
self.write(&type_decl.name.name);
|
||||
|
||||
// Type parameters
|
||||
if !type_decl.type_params.is_empty() {
|
||||
self.write("<");
|
||||
self.write(
|
||||
&type_decl
|
||||
.type_params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(">");
|
||||
}
|
||||
|
||||
self.write(" =");
|
||||
|
||||
match &type_decl.definition {
|
||||
TypeDef::Alias(t) => {
|
||||
self.write(" ");
|
||||
self.writeln(&self.format_type_expr(t));
|
||||
}
|
||||
TypeDef::Record(fields) => {
|
||||
self.writeln(" {");
|
||||
self.indent_level += 1;
|
||||
for field in fields {
|
||||
self.write(&self.indent());
|
||||
self.write(&field.name.name);
|
||||
self.write(": ");
|
||||
self.write(&self.format_type_expr(&field.typ));
|
||||
self.writeln(",");
|
||||
}
|
||||
self.indent_level -= 1;
|
||||
self.write(&self.indent());
|
||||
self.writeln("}");
|
||||
}
|
||||
TypeDef::Enum(variants) => {
|
||||
self.newline();
|
||||
self.indent_level += 1;
|
||||
for variant in variants {
|
||||
self.write(&self.indent());
|
||||
self.write("| ");
|
||||
self.write(&variant.name.name);
|
||||
match &variant.fields {
|
||||
VariantFields::Unit => {}
|
||||
VariantFields::Tuple(types) => {
|
||||
self.write("(");
|
||||
self.write(
|
||||
&types
|
||||
.iter()
|
||||
.map(|t| self.format_type_expr(t))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(")");
|
||||
}
|
||||
VariantFields::Record(fields) => {
|
||||
self.write(" { ");
|
||||
self.write(
|
||||
&fields
|
||||
.iter()
|
||||
.map(|f| format!("{}: {}", f.name.name, self.format_type_expr(&f.typ)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(" }");
|
||||
}
|
||||
}
|
||||
self.newline();
|
||||
}
|
||||
self.indent_level -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_effect(&mut self, effect: &EffectDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("effect ");
|
||||
self.write(&effect.name.name);
|
||||
|
||||
if !effect.type_params.is_empty() {
|
||||
self.write("<");
|
||||
self.write(
|
||||
&effect
|
||||
.type_params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(">");
|
||||
}
|
||||
|
||||
self.writeln(" {");
|
||||
self.indent_level += 1;
|
||||
|
||||
for op in &effect.operations {
|
||||
self.write(&self.indent());
|
||||
self.write("fn ");
|
||||
self.write(&op.name.name);
|
||||
self.write("(");
|
||||
let params: Vec<String> = op
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
self.write(¶ms.join(", "));
|
||||
self.write("): ");
|
||||
self.writeln(&self.format_type_expr(&op.return_type));
|
||||
}
|
||||
|
||||
self.indent_level -= 1;
|
||||
self.write(&self.indent());
|
||||
self.writeln("}");
|
||||
}
|
||||
|
||||
fn format_handler(&mut self, handler: &HandlerDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("handler ");
|
||||
self.write(&handler.name.name);
|
||||
self.write(": ");
|
||||
self.write(&handler.effect.name);
|
||||
self.writeln(" {");
|
||||
self.indent_level += 1;
|
||||
|
||||
for impl_ in &handler.implementations {
|
||||
self.write(&self.indent());
|
||||
self.write("fn ");
|
||||
self.write(&impl_.op_name.name);
|
||||
self.write("(");
|
||||
self.write(
|
||||
&impl_
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(") = ");
|
||||
|
||||
let body_str = self.format_expr(&impl_.body);
|
||||
if body_str.contains('\n') {
|
||||
self.newline();
|
||||
self.indent_level += 1;
|
||||
self.write(&self.indent());
|
||||
self.write(&body_str);
|
||||
self.indent_level -= 1;
|
||||
} else {
|
||||
self.write(&body_str);
|
||||
}
|
||||
self.newline();
|
||||
}
|
||||
|
||||
self.indent_level -= 1;
|
||||
self.write(&self.indent());
|
||||
self.writeln("}");
|
||||
}
|
||||
|
||||
fn format_trait(&mut self, trait_decl: &TraitDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("trait ");
|
||||
self.write(&trait_decl.name.name);
|
||||
|
||||
if !trait_decl.type_params.is_empty() {
|
||||
self.write("<");
|
||||
self.write(
|
||||
&trait_decl
|
||||
.type_params
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
self.write(">");
|
||||
}
|
||||
|
||||
self.writeln(" {");
|
||||
self.indent_level += 1;
|
||||
|
||||
for method in &trait_decl.methods {
|
||||
self.write(&self.indent());
|
||||
self.write("fn ");
|
||||
self.write(&method.name.name);
|
||||
self.write("(");
|
||||
let params: Vec<String> = method
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
self.write(¶ms.join(", "));
|
||||
self.write("): ");
|
||||
self.writeln(&self.format_type_expr(&method.return_type));
|
||||
}
|
||||
|
||||
self.indent_level -= 1;
|
||||
self.write(&self.indent());
|
||||
self.writeln("}");
|
||||
}
|
||||
|
||||
fn format_impl(&mut self, impl_decl: &ImplDecl) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("impl ");
|
||||
self.write(&impl_decl.trait_name.name);
|
||||
self.write(" for ");
|
||||
self.write(&self.format_type_expr(&impl_decl.target_type));
|
||||
self.writeln(" {");
|
||||
self.indent_level += 1;
|
||||
|
||||
for method in &impl_decl.methods {
|
||||
self.format_impl_method(method);
|
||||
}
|
||||
|
||||
self.indent_level -= 1;
|
||||
self.write(&self.indent());
|
||||
self.writeln("}");
|
||||
}
|
||||
|
||||
fn format_impl_method(&mut self, method: &ImplMethod) {
|
||||
let indent = self.indent();
|
||||
self.write(&indent);
|
||||
self.write("fn ");
|
||||
self.write(&method.name.name);
|
||||
self.write("(");
|
||||
let params: Vec<String> = method
|
||||
.params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
self.write(¶ms.join(", "));
|
||||
self.write(")");
|
||||
|
||||
if let Some(ref ret) = method.return_type {
|
||||
self.write(": ");
|
||||
self.write(&self.format_type_expr(ret));
|
||||
}
|
||||
|
||||
self.write(" = ");
|
||||
|
||||
let body_str = self.format_expr(&method.body);
|
||||
if self.is_block_expr(&method.body) || body_str.contains('\n') {
|
||||
self.newline();
|
||||
self.indent_level += 1;
|
||||
self.write(&self.indent());
|
||||
self.write(&body_str);
|
||||
self.indent_level -= 1;
|
||||
} else {
|
||||
self.write(&body_str);
|
||||
}
|
||||
self.newline();
|
||||
}
|
||||
|
||||
fn format_property(&self, prop: &BehavioralProperty) -> String {
|
||||
match prop {
|
||||
BehavioralProperty::Pure => "pure".to_string(),
|
||||
BehavioralProperty::Total => "total".to_string(),
|
||||
BehavioralProperty::Idempotent => "idempotent".to_string(),
|
||||
BehavioralProperty::Deterministic => "deterministic".to_string(),
|
||||
BehavioralProperty::Commutative => "commutative".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_type_expr(&self, typ: &TypeExpr) -> String {
|
||||
match typ {
|
||||
TypeExpr::Named(ident) => ident.name.clone(),
|
||||
TypeExpr::App(base, args) => {
|
||||
format!(
|
||||
"{}<{}>",
|
||||
self.format_type_expr(base),
|
||||
args.iter()
|
||||
.map(|a| self.format_type_expr(a))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
TypeExpr::Function { params, return_type, effects } => {
|
||||
let mut s = format!(
|
||||
"fn({}): {}",
|
||||
params
|
||||
.iter()
|
||||
.map(|p| self.format_type_expr(p))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
self.format_type_expr(return_type)
|
||||
);
|
||||
if !effects.is_empty() {
|
||||
s.push_str(" with {");
|
||||
s.push_str(
|
||||
&effects
|
||||
.iter()
|
||||
.map(|e| e.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
);
|
||||
s.push('}');
|
||||
}
|
||||
s
|
||||
}
|
||||
TypeExpr::Tuple(types) => {
|
||||
format!(
|
||||
"({})",
|
||||
types
|
||||
.iter()
|
||||
.map(|t| self.format_type_expr(t))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
TypeExpr::Record(fields) => {
|
||||
format!(
|
||||
"{{ {} }}",
|
||||
fields
|
||||
.iter()
|
||||
.map(|f| format!("{}: {}", f.name.name, self.format_type_expr(&f.typ)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
TypeExpr::Unit => "Unit".to_string(),
|
||||
TypeExpr::Versioned { base, constraint } => {
|
||||
let mut s = self.format_type_expr(base);
|
||||
s.push_str(&format!(" {}", constraint));
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_block_expr(&self, expr: &Expr) -> bool {
|
||||
matches!(expr, Expr::Block { .. } | Expr::Match { .. })
|
||||
}
|
||||
|
||||
fn format_expr(&self, expr: &Expr) -> String {
|
||||
match expr {
|
||||
Expr::Literal(lit) => self.format_literal(lit),
|
||||
Expr::Var(ident) => ident.name.clone(),
|
||||
Expr::BinaryOp { left, op, right, .. } => {
|
||||
format!(
|
||||
"{} {} {}",
|
||||
self.format_expr(left),
|
||||
self.format_binop(op),
|
||||
self.format_expr(right)
|
||||
)
|
||||
}
|
||||
Expr::UnaryOp { op, operand, .. } => {
|
||||
format!("{}{}", self.format_unaryop(op), self.format_expr(operand))
|
||||
}
|
||||
Expr::Call { func, args, .. } => {
|
||||
format!(
|
||||
"{}({})",
|
||||
self.format_expr(func),
|
||||
args.iter()
|
||||
.map(|a| self.format_expr(a))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Expr::Field { object, field, .. } => {
|
||||
format!("{}.{}", self.format_expr(object), field.name)
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
format!(
|
||||
"if {} then {} else {}",
|
||||
self.format_expr(condition),
|
||||
self.format_expr(then_branch),
|
||||
self.format_expr(else_branch)
|
||||
)
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
let mut s = format!("match {} {{\n", self.format_expr(scrutinee));
|
||||
for arm in arms {
|
||||
s.push_str(&format!(
|
||||
" {} => {},\n",
|
||||
self.format_pattern(&arm.pattern),
|
||||
self.format_expr(&arm.body)
|
||||
));
|
||||
}
|
||||
s.push('}');
|
||||
s
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
let mut s = String::from("{\n");
|
||||
for stmt in statements {
|
||||
match stmt {
|
||||
Statement::Let { name, typ, value, .. } => {
|
||||
let type_str = typ.as_ref()
|
||||
.map(|t| format!(": {}", self.format_type_expr(t)))
|
||||
.unwrap_or_default();
|
||||
s.push_str(&format!(
|
||||
" let {}{} = {}\n",
|
||||
name.name,
|
||||
type_str,
|
||||
self.format_expr(value)
|
||||
));
|
||||
}
|
||||
Statement::Expr(e) => {
|
||||
s.push_str(&format!(" {}\n", self.format_expr(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
s.push_str(&format!(" {}\n", self.format_expr(result)));
|
||||
s.push('}');
|
||||
s
|
||||
}
|
||||
Expr::Lambda { params, body, return_type, .. } => {
|
||||
let params_str: Vec<String> = params
|
||||
.iter()
|
||||
.map(|p| format!("{}: {}", p.name.name, self.format_type_expr(&p.typ)))
|
||||
.collect();
|
||||
let ret_str = return_type
|
||||
.as_ref()
|
||||
.map(|t| format!(": {}", self.format_type_expr(t)))
|
||||
.unwrap_or_default();
|
||||
format!("fn({}){} => {}", params_str.join(", "), ret_str, self.format_expr(body))
|
||||
}
|
||||
Expr::Let { name, value, body, typ, .. } => {
|
||||
let type_str = typ.as_ref()
|
||||
.map(|t| format!(": {}", self.format_type_expr(t)))
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"let {}{} = {}; {}",
|
||||
name.name,
|
||||
type_str,
|
||||
self.format_expr(value),
|
||||
self.format_expr(body)
|
||||
)
|
||||
}
|
||||
Expr::List { elements, .. } => {
|
||||
format!(
|
||||
"[{}]",
|
||||
elements
|
||||
.iter()
|
||||
.map(|e| self.format_expr(e))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Expr::Tuple { elements, .. } => {
|
||||
format!(
|
||||
"({})",
|
||||
elements
|
||||
.iter()
|
||||
.map(|e| self.format_expr(e))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
format!(
|
||||
"{{ {} }}",
|
||||
fields
|
||||
.iter()
|
||||
.map(|(name, val)| format!("{}: {}", name.name, self.format_expr(val)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Expr::EffectOp { effect, operation, args, .. } => {
|
||||
format!(
|
||||
"{}.{}({})",
|
||||
effect.name,
|
||||
operation.name,
|
||||
args.iter()
|
||||
.map(|a| self.format_expr(a))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Expr::Run { expr, handlers, .. } => {
|
||||
let mut s = format!("run {} with {{\n", self.format_expr(expr));
|
||||
for (effect, handler) in handlers {
|
||||
s.push_str(&format!(" {} = {},\n", effect.name, self.format_expr(handler)));
|
||||
}
|
||||
s.push('}');
|
||||
s
|
||||
}
|
||||
Expr::Resume { value, .. } => {
|
||||
format!("resume({})", self.format_expr(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_literal(&self, lit: &Literal) -> String {
|
||||
match &lit.kind {
|
||||
LiteralKind::Int(n) => n.to_string(),
|
||||
LiteralKind::Float(f) => format!("{}", f),
|
||||
LiteralKind::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
|
||||
LiteralKind::Char(c) => format!("'{}'", c),
|
||||
LiteralKind::Bool(b) => b.to_string(),
|
||||
LiteralKind::Unit => "()".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_binop(&self, op: &BinaryOp) -> &'static str {
|
||||
match op {
|
||||
BinaryOp::Add => "+",
|
||||
BinaryOp::Sub => "-",
|
||||
BinaryOp::Mul => "*",
|
||||
BinaryOp::Div => "/",
|
||||
BinaryOp::Mod => "%",
|
||||
BinaryOp::Eq => "==",
|
||||
BinaryOp::Ne => "!=",
|
||||
BinaryOp::Lt => "<",
|
||||
BinaryOp::Le => "<=",
|
||||
BinaryOp::Gt => ">",
|
||||
BinaryOp::Ge => ">=",
|
||||
BinaryOp::And => "&&",
|
||||
BinaryOp::Or => "||",
|
||||
BinaryOp::Pipe => "|>",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_unaryop(&self, op: &UnaryOp) -> &'static str {
|
||||
match op {
|
||||
UnaryOp::Neg => "-",
|
||||
UnaryOp::Not => "!",
|
||||
}
|
||||
}
|
||||
|
||||
fn format_pattern(&self, pattern: &Pattern) -> String {
|
||||
match pattern {
|
||||
Pattern::Wildcard(_) => "_".to_string(),
|
||||
Pattern::Var(ident) => ident.name.clone(),
|
||||
Pattern::Literal(lit) => self.format_literal(lit),
|
||||
Pattern::Constructor { name, fields, .. } => {
|
||||
if fields.is_empty() {
|
||||
name.name.clone()
|
||||
} else {
|
||||
format!(
|
||||
"{}({})",
|
||||
name.name,
|
||||
fields
|
||||
.iter()
|
||||
.map(|f| self.format_pattern(f))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
Pattern::Tuple { elements, .. } => {
|
||||
format!(
|
||||
"({})",
|
||||
elements
|
||||
.iter()
|
||||
.map(|e| self.format_pattern(e))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
Pattern::Record { fields, .. } => {
|
||||
format!(
|
||||
"{{ {} }}",
|
||||
fields
|
||||
.iter()
|
||||
.map(|(name, pat)| {
|
||||
format!("{}: {}", name.name, self.format_pattern(pat))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_simple_function() {
|
||||
let source = "fn add( x:Int,y:Int ):Int=x+y";
|
||||
let result = format(source, &FormatConfig::default()).unwrap();
|
||||
assert!(result.contains("fn add(x: Int, y: Int): Int = x + y"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_let() {
|
||||
let source = "let x=42";
|
||||
let result = format(source, &FormatConfig::default()).unwrap();
|
||||
assert!(result.contains("let x = 42"));
|
||||
}
|
||||
}
|
||||
373
src/main.rs
373
src/main.rs
@@ -3,6 +3,7 @@
|
||||
mod ast;
|
||||
mod diagnostics;
|
||||
mod exhaustiveness;
|
||||
mod formatter;
|
||||
mod interpreter;
|
||||
mod lexer;
|
||||
mod lsp;
|
||||
@@ -77,17 +78,49 @@ fn main() {
|
||||
}
|
||||
}
|
||||
"--help" | "-h" => {
|
||||
println!("Lux {} - A functional language with first-class effects", VERSION);
|
||||
println!();
|
||||
println!("Usage:");
|
||||
println!(" lux Start the REPL");
|
||||
println!(" lux <file.lux> Run a file");
|
||||
println!(" lux --lsp Start LSP server (for IDE integration)");
|
||||
println!(" lux --help Show this help");
|
||||
print_help();
|
||||
}
|
||||
"--version" | "-v" => {
|
||||
println!("Lux {}", VERSION);
|
||||
}
|
||||
"fmt" => {
|
||||
// Format files
|
||||
if args.len() < 3 {
|
||||
eprintln!("Usage: lux fmt <file.lux> [--check]");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let check_only = args.iter().any(|a| a == "--check");
|
||||
for arg in &args[2..] {
|
||||
if arg.starts_with('-') {
|
||||
continue;
|
||||
}
|
||||
format_file(arg, check_only);
|
||||
}
|
||||
}
|
||||
"test" => {
|
||||
// Run tests
|
||||
run_tests(&args[2..]);
|
||||
}
|
||||
"watch" => {
|
||||
// Watch mode
|
||||
if args.len() < 3 {
|
||||
eprintln!("Usage: lux watch <file.lux>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
watch_file(&args[2]);
|
||||
}
|
||||
"init" => {
|
||||
// Initialize a new project
|
||||
init_project(args.get(2).map(|s| s.as_str()));
|
||||
}
|
||||
"check" => {
|
||||
// Type check without running
|
||||
if args.len() < 3 {
|
||||
eprintln!("Usage: lux check <file.lux>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
check_file(&args[2]);
|
||||
}
|
||||
path => {
|
||||
// Run a file
|
||||
run_file(path);
|
||||
@@ -99,6 +132,332 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("Lux {} - A functional language with first-class effects", VERSION);
|
||||
println!();
|
||||
println!("Usage:");
|
||||
println!(" lux Start the REPL");
|
||||
println!(" lux <file.lux> Run a file");
|
||||
println!(" lux fmt <file.lux> Format a file (--check to verify only)");
|
||||
println!(" lux check <file.lux> Type check without running");
|
||||
println!(" lux test [pattern] Run tests (optional pattern filter)");
|
||||
println!(" lux watch <file.lux> Watch and re-run on changes");
|
||||
println!(" lux init [name] Initialize a new project");
|
||||
println!(" lux --lsp Start LSP server (for IDE integration)");
|
||||
println!(" lux --help Show this help");
|
||||
println!(" lux --version Show version");
|
||||
}
|
||||
|
||||
fn format_file(path: &str, check_only: bool) {
|
||||
use formatter::{format, FormatConfig};
|
||||
|
||||
let source = match std::fs::read_to_string(path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading file '{}': {}", path, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let config = FormatConfig::default();
|
||||
let formatted = match format(&source, &config) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Error formatting '{}': {}", path, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if check_only {
|
||||
if source != formatted {
|
||||
eprintln!("{} would be reformatted", path);
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
println!("{} is correctly formatted", path);
|
||||
}
|
||||
} else {
|
||||
if source != formatted {
|
||||
if let Err(e) = std::fs::write(path, &formatted) {
|
||||
eprintln!("Error writing file '{}': {}", path, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("Formatted {}", path);
|
||||
} else {
|
||||
println!("{} unchanged", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_file(path: &str) {
|
||||
use modules::ModuleLoader;
|
||||
use std::path::Path;
|
||||
|
||||
let file_path = Path::new(path);
|
||||
let source = match std::fs::read_to_string(file_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading file '{}': {}", path, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut loader = ModuleLoader::new();
|
||||
if let Some(parent) = file_path.parent() {
|
||||
loader.add_search_path(parent.to_path_buf());
|
||||
}
|
||||
|
||||
let program = match loader.load_source(&source, Some(file_path)) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("Module error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut checker = TypeChecker::new();
|
||||
if let Err(errors) = checker.check_program_with_modules(&program, &loader) {
|
||||
for error in errors {
|
||||
let diagnostic = error.to_diagnostic();
|
||||
eprint!("{}", render(&diagnostic, &source, Some(path)));
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("{}: OK", path);
|
||||
}
|
||||
|
||||
fn run_tests(args: &[String]) {
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
|
||||
// Find test files
|
||||
let pattern = args.first().map(|s| s.as_str());
|
||||
|
||||
// Look for test files in current directory and tests/ subdirectory
|
||||
let mut test_files = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(".").into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.starts_with("test_") || name.ends_with("_test.lux") {
|
||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
||||
test_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Path::new("tests").is_dir() {
|
||||
for entry in fs::read_dir("tests").into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
||||
test_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if test_files.is_empty() {
|
||||
println!("No test files found.");
|
||||
println!("Test files should be named test_*.lux or *_test.lux");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut passed = 0;
|
||||
let mut failed = 0;
|
||||
|
||||
for test_file in &test_files {
|
||||
let path_str = test_file.to_string_lossy().to_string();
|
||||
print!("Testing {}... ", path_str);
|
||||
|
||||
// Run the test file
|
||||
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.arg(&path_str)
|
||||
.output();
|
||||
|
||||
match result {
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("OK");
|
||||
passed += 1;
|
||||
}
|
||||
Ok(output) => {
|
||||
println!("FAILED");
|
||||
if !output.stderr.is_empty() {
|
||||
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
failed += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("ERROR: {}", e);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Results: {} passed, {} failed", passed, failed);
|
||||
|
||||
if failed > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_file(path: &str) {
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::path::Path;
|
||||
|
||||
let file_path = Path::new(path);
|
||||
if !file_path.exists() {
|
||||
eprintln!("File not found: {}", path);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("Watching {} for changes (Ctrl+C to stop)...", path);
|
||||
println!();
|
||||
|
||||
let mut last_modified = SystemTime::UNIX_EPOCH;
|
||||
|
||||
loop {
|
||||
let metadata = match std::fs::metadata(file_path) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
|
||||
if modified > last_modified {
|
||||
last_modified = modified;
|
||||
|
||||
// Clear screen
|
||||
print!("\x1B[2J\x1B[H");
|
||||
|
||||
println!("=== Running {} ===", path);
|
||||
println!();
|
||||
|
||||
// Run the file
|
||||
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.arg(path)
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) if status.success() => {
|
||||
println!();
|
||||
println!("=== Success ===");
|
||||
}
|
||||
Ok(_) => {
|
||||
println!();
|
||||
println!("=== Failed ===");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error running file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Watching for changes...");
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
|
||||
fn init_project(name: Option<&str>) {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let project_name = name.unwrap_or("my-lux-project");
|
||||
let project_dir = Path::new(project_name);
|
||||
|
||||
if project_dir.exists() {
|
||||
eprintln!("Directory '{}' already exists", project_name);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Create project structure
|
||||
fs::create_dir_all(project_dir.join("src")).unwrap();
|
||||
fs::create_dir_all(project_dir.join("tests")).unwrap();
|
||||
|
||||
// Create lux.toml
|
||||
let toml_content = format!(
|
||||
r#"[project]
|
||||
name = "{}"
|
||||
version = "0.1.0"
|
||||
description = "A Lux project"
|
||||
|
||||
[dependencies]
|
||||
# Add dependencies here
|
||||
# example = "1.0.0"
|
||||
"#,
|
||||
project_name
|
||||
);
|
||||
fs::write(project_dir.join("lux.toml"), toml_content).unwrap();
|
||||
|
||||
// Create main.lux
|
||||
let main_content = r#"// Main entry point
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
Console.print("Hello from Lux!")
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
"#;
|
||||
fs::write(project_dir.join("src").join("main.lux"), main_content).unwrap();
|
||||
|
||||
// Create a test file
|
||||
let test_content = r#"// Example test file
|
||||
|
||||
fn testAddition(): Bool = {
|
||||
let result = 2 + 2
|
||||
result == 4
|
||||
}
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
if testAddition() then
|
||||
Console.print("Test passed!")
|
||||
else
|
||||
Console.print("Test failed!")
|
||||
}
|
||||
|
||||
let output = run main() with {}
|
||||
"#;
|
||||
fs::write(project_dir.join("tests").join("test_example.lux"), test_content).unwrap();
|
||||
|
||||
// Create .gitignore
|
||||
let gitignore_content = r#"# Lux build artifacts
|
||||
/target/
|
||||
*.luxc
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
"#;
|
||||
fs::write(project_dir.join(".gitignore"), gitignore_content).unwrap();
|
||||
|
||||
println!("Created new Lux project: {}", project_name);
|
||||
println!();
|
||||
println!("Project structure:");
|
||||
println!(" {}/", project_name);
|
||||
println!(" ├── lux.toml");
|
||||
println!(" ├── src/");
|
||||
println!(" │ └── main.lux");
|
||||
println!(" └── tests/");
|
||||
println!(" └── test_example.lux");
|
||||
println!();
|
||||
println!("To get started:");
|
||||
println!(" cd {}", project_name);
|
||||
println!(" lux src/main.lux");
|
||||
}
|
||||
|
||||
fn run_file(path: &str) {
|
||||
use modules::ModuleLoader;
|
||||
use std::path::Path;
|
||||
|
||||
Reference in New Issue
Block a user