diff --git a/editors/nvim/README.md b/editors/nvim/README.md new file mode 100644 index 0000000..6b293fe --- /dev/null +++ b/editors/nvim/README.md @@ -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 ``): + +| Mapping | Action | +|---------|--------| +| `r` | Run current file | +| `f` | Format current file | +| `t` | Run tests | + +LSP mappings (when LSP is active): + +| Mapping | Action | +|---------|--------| +| `gd` | Go to definition | +| `K` | Hover information | +| `gr` | Find references | +| `rn` | Rename symbol | +| `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, + }, +}) +``` diff --git a/editors/nvim/after/queries/lux/highlights.scm b/editors/nvim/after/queries/lux/highlights.scm new file mode 100644 index 0000000..7b0677c --- /dev/null +++ b/editors/nvim/after/queries/lux/highlights.scm @@ -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) diff --git a/editors/nvim/after/queries/lux/indents.scm b/editors/nvim/after/queries/lux/indents.scm new file mode 100644 index 0000000..bcbd59a --- /dev/null +++ b/editors/nvim/after/queries/lux/indents.scm @@ -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) diff --git a/editors/nvim/after/queries/lux/locals.scm b/editors/nvim/after/queries/lux/locals.scm new file mode 100644 index 0000000..74ded67 --- /dev/null +++ b/editors/nvim/after/queries/lux/locals.scm @@ -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 diff --git a/editors/nvim/ftdetect/lux.vim b/editors/nvim/ftdetect/lux.vim new file mode 100644 index 0000000..8e0425a --- /dev/null +++ b/editors/nvim/ftdetect/lux.vim @@ -0,0 +1,2 @@ +" File type detection for Lux +autocmd BufRead,BufNewFile *.lux setfiletype lux diff --git a/editors/nvim/ftplugin/lux.vim b/editors/nvim/ftplugin/lux.vim new file mode 100644 index 0000000..eb7db90 --- /dev/null +++ b/editors/nvim/ftplugin/lux.vim @@ -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 r :!lux % +nnoremap f :!lux fmt % +nnoremap t :!lux test diff --git a/editors/nvim/lua/lux/init.lua b/editors/nvim/lua/lux/init.lua new file mode 100644 index 0000000..0e9d116 --- /dev/null +++ b/editors/nvim/lua/lux/init.lua @@ -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", "", vim.lsp.buf.signature_help, opts) + vim.keymap.set("n", "gr", vim.lsp.buf.references, opts) + vim.keymap.set("n", "rn", vim.lsp.buf.rename, opts) + vim.keymap.set("n", "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 diff --git a/editors/nvim/syntax/lux.vim b/editors/nvim/syntax/lux.vim new file mode 100644 index 0000000..0a5f6cc --- /dev/null +++ b/editors/nvim/syntax/lux.vim @@ -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 /\/ + +" 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" diff --git a/editors/tree-sitter-lux/grammar.js b/editors/tree-sitter-lux/grammar.js new file mode 100644 index 0000000..fdac8ed --- /dev/null +++ b/editors/tree-sitter-lux/grammar.js @@ -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_]*/, + } +}); diff --git a/editors/tree-sitter-lux/package.json b/editors/tree-sitter-lux/package.json new file mode 100644 index 0000000..8212c91 --- /dev/null +++ b/editors/tree-sitter-lux/package.json @@ -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" + } + ] +} diff --git a/editors/tree-sitter-lux/queries/highlights.scm b/editors/tree-sitter-lux/queries/highlights.scm new file mode 100644 index 0000000..7b0677c --- /dev/null +++ b/editors/tree-sitter-lux/queries/highlights.scm @@ -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) diff --git a/editors/tree-sitter-lux/queries/indents.scm b/editors/tree-sitter-lux/queries/indents.scm new file mode 100644 index 0000000..bcbd59a --- /dev/null +++ b/editors/tree-sitter-lux/queries/indents.scm @@ -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) diff --git a/editors/tree-sitter-lux/queries/locals.scm b/editors/tree-sitter-lux/queries/locals.scm new file mode 100644 index 0000000..74ded67 --- /dev/null +++ b/editors/tree-sitter-lux/queries/locals.scm @@ -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 diff --git a/src/formatter.rs b/src/formatter.rs new file mode 100644 index 0000000..e1d9488 --- /dev/null +++ b/src/formatter.rs @@ -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 { + // 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::>() + .join(", "), + ); + self.write(">"); + } + + // Parameters + self.write("("); + let params: Vec = 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::>() + .join(", "), + ); + self.write("}"); + } + + // Properties + if !func.properties.is_empty() { + self.write(" is "); + self.write( + &func + .properties + .iter() + .map(|p| self.format_property(p)) + .collect::>() + .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::>() + .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::>() + .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::>() + .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::>() + .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 = 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::>() + .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::>() + .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 = 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 = 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::>() + .join(", ") + ) + } + TypeExpr::Function { params, return_type, effects } => { + let mut s = format!( + "fn({}): {}", + params + .iter() + .map(|p| self.format_type_expr(p)) + .collect::>() + .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::>() + .join(", "), + ); + s.push('}'); + } + s + } + TypeExpr::Tuple(types) => { + format!( + "({})", + types + .iter() + .map(|t| self.format_type_expr(t)) + .collect::>() + .join(", ") + ) + } + TypeExpr::Record(fields) => { + format!( + "{{ {} }}", + fields + .iter() + .map(|f| format!("{}: {}", f.name.name, self.format_type_expr(&f.typ))) + .collect::>() + .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::>() + .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 = 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::>() + .join(", ") + ) + } + Expr::Tuple { elements, .. } => { + format!( + "({})", + elements + .iter() + .map(|e| self.format_expr(e)) + .collect::>() + .join(", ") + ) + } + Expr::Record { fields, .. } => { + format!( + "{{ {} }}", + fields + .iter() + .map(|(name, val)| format!("{}: {}", name.name, self.format_expr(val))) + .collect::>() + .join(", ") + ) + } + Expr::EffectOp { effect, operation, args, .. } => { + format!( + "{}.{}({})", + effect.name, + operation.name, + args.iter() + .map(|a| self.format_expr(a)) + .collect::>() + .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::>() + .join(", ") + ) + } + } + Pattern::Tuple { elements, .. } => { + format!( + "({})", + elements + .iter() + .map(|e| self.format_pattern(e)) + .collect::>() + .join(", ") + ) + } + Pattern::Record { fields, .. } => { + format!( + "{{ {} }}", + fields + .iter() + .map(|(name, pat)| { + format!("{}: {}", name.name, self.format_pattern(pat)) + }) + .collect::>() + .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")); + } +} diff --git a/src/main.rs b/src/main.rs index 24636c4..28df795 100644 --- a/src/main.rs +++ b/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 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 [--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 "); + 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 "); + 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 Run a file"); + println!(" lux fmt Format a file (--check to verify only)"); + println!(" lux check Type check without running"); + println!(" lux test [pattern] Run tests (optional pattern filter)"); + println!(" lux watch 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;