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:
2026-02-13 10:30:13 -05:00
parent 961a861822
commit f786d18182
15 changed files with 2578 additions and 7 deletions

134
editors/nvim/README.md Normal file
View 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,
},
})
```

View 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)

View 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)

View 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

View File

@@ -0,0 +1,2 @@
" File type detection for Lux
autocmd BufRead,BufNewFile *.lux setfiletype lux

View 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>

View 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

View 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"

View 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_]*/,
}
});

View 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"
}
]
}

View 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)

View 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)

View 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
View 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(&params.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(&params.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(&params.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(&params.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"));
}
}

View File

@@ -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;