diff --git a/lib/rouge/demos/rell b/lib/rouge/demos/rell new file mode 100644 index 0000000000..d9576edecd --- /dev/null +++ b/lib/rouge/demos/rell @@ -0,0 +1,20 @@ +// Define an entity with a key and mutable attribute +entity user { + key name: text; + mutable balance: integer = 0; +} + +// Query to find a user by name +query get_user(name: text) { + return user @? { .name == name }; +} + +// Operation to create a new user +operation create_user(name: text, initial_balance: integer) { + create user(name, balance = initial_balance); +} + +// Operation to update user balance +operation update_balance(name: text, amount: integer) { + update user @? { .name == name } ( balance += amount ); +} diff --git a/lib/rouge/lexers/rell.rb b/lib/rouge/lexers/rell.rb new file mode 100644 index 0000000000..e181951915 --- /dev/null +++ b/lib/rouge/lexers/rell.rb @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +module Rouge + module Lexers + class Rell < RegexLexer + title "Rell" + desc "The Rell programming language (https://docs.chromia.com/rell/rell-intro)" + tag 'rell' + filenames '*.rell' + mimetypes 'text/x-rell' + + def self.keywords + @keywords ||= %w( + abstract break class continue create delete else entity enum + false for function guard if import in include index key limit + module mutable namespace null object offset operation override + query record return struct true update val var when while and or not + ) + end + + def self.builtins + @builtins ||= %w( + big_integer boolean byte_array decimal gtv integer json list + map rowid set text iterable collection unit range tuple virtual + ) + end + + id = /[a-zA-Z_][a-zA-Z0-9_]*/ + + state :root do + rule %r/\s+/, Text + + # Comments + rule %r(//.*?$), Comment::Single + rule %r(/\*), Comment::Multiline, :comment + + # Annotations + rule %r/@#{id}/, Name::Decorator + + # At-expressions (special operators) + rule %r/@[*+?]/, Operator + rule %r/@/, Operator + + # Byte array literals (must come before identifier matching) + rule %r/x'[0-9a-fA-F]*'/, Str::Other + rule %r/x"[0-9a-fA-F]*"/, Str::Other + + # Keywords + rule %r/\b(entity|enum|namespace|object|struct)\b/, Keyword::Declaration + rule %r/\b(function|operation|query)\b/ do + token Keyword::Declaration + push :function_name + end + + rule id do |m| + name = m[0] + if self.class.keywords.include?(name) + token Keyword + elsif self.class.builtins.include?(name) + token Keyword::Type + else + token Name + end + end + + # String literals + rule %r/'/, Str::Single, :string_single + rule %r/"/, Str::Double, :string_double + + # Numeric literals + # Hexadecimal + rule %r/-?0[xX][0-9a-fA-F]+/, Num::Hex + + # Integers with exponent and L suffix (bigint) + rule %r/-?\d+[eE][+-]?\d+[lL]/, Num::Integer + + # Decimals with exponent (no L suffix) + rule %r/-?\d+[eE][+-]?\d+/, Num::Float + + # Decimal with decimal point + rule %r/-?\d*\.\d+(?:[eE][+-]?\d+)?/, Num::Float + + # Plain integers (with optional L suffix) + rule %r/-?\d+[lL]?/, Num::Integer + + # Operators (long operators first) + rule %r/===|!==/, Operator + rule %r/==|!=|<=|>=/, Operator + rule %r/\+=|-=|\*=|\/=|%=/, Operator + rule %r/\+\+|--/, Operator + rule %r/\?\.|!!|\?:|\?\?/, Operator + rule %r/->/, Operator + rule %r/[+\-*\/%<>=!?]/, Operator + + # Special characters + rule %r/\$/, Operator + rule %r/\^/, Operator + + # Attribute access + rule %r/(\.)(\s*)(#{id})/ do + groups Punctuation, Text, Name::Attribute + end + + # Punctuation + rule %r/[{}()\[\];:,.]/, Punctuation + end + + state :function_name do + rule %r/\s+/, Text + rule id, Name::Function, :pop! + rule(//) { pop! } + end + + state :comment do + rule %r(/\*), Comment::Multiline, :comment + rule %r(\*/), Comment::Multiline, :pop! + rule %r([^/*]+), Comment::Multiline + rule %r([/*]), Comment::Multiline + end + + state :string_double do + rule %r/[^\\"]+/, Str::Double + rule %r/\\[\\'"nrtbf]/, Str::Escape + rule %r/\\u[0-9a-fA-F]{4}/, Str::Escape + rule %r/\\U[0-9a-fA-F]{8}/, Str::Escape + rule %r/\\./, Str::Escape + rule %r/"/, Str::Double, :pop! + end + + state :string_single do + rule %r/[^\\']+/, Str::Single + rule %r/\\[\\'"nrtbf]/, Str::Escape + rule %r/\\u[0-9a-fA-F]{4}/, Str::Escape + rule %r/\\U[0-9a-fA-F]{8}/, Str::Escape + rule %r/\\./, Str::Escape + rule %r/'/, Str::Single, :pop! + end + end + end +end diff --git a/spec/lexers/rell_spec.rb b/spec/lexers/rell_spec.rb new file mode 100644 index 0000000000..1b72e29218 --- /dev/null +++ b/spec/lexers/rell_spec.rb @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +describe Rouge::Lexers::Rell do + let(:subject) { Rouge::Lexers::Rell.new } + + describe 'guessing' do + include Support::Guessing + + it 'guesses by filename' do + assert_guess :filename => 'foo.rell' + end + + it 'guesses by mimetype' do + assert_guess :mimetype => 'text/x-rell' + end + end + + describe 'lexing' do + include Support::Lexing + + it 'recognizes comments' do + assert_tokens_equal '// comment', ['Comment.Single', '// comment'] + end + + it 'recognizes at-expressions' do + assert_tokens_equal '@', %w[Operator @] + assert_tokens_equal '@?', %w[Operator @?] + assert_tokens_equal '@*', %w[Operator @*] + assert_tokens_equal '@+', %w[Operator @+] + end + + it 'recognizes annotations' do + assert_tokens_equal '@log', %w[Name.Decorator @log] + end + + it 'recognizes numeric literals' do + assert_tokens_equal '123', %w[Literal.Number.Integer 123] + assert_tokens_equal '3.14', %w[Literal.Number.Float 3.14] + assert_tokens_equal '123L', %w[Literal.Number.Integer 123L] + assert_tokens_equal '-42', %w[Literal.Number.Integer -42] + assert_tokens_equal '1E1', %w[Literal.Number.Float 1E1] + assert_tokens_equal '1e1', %w[Literal.Number.Float 1e1] + assert_tokens_equal '1E+1', %w[Literal.Number.Float 1E+1] + assert_tokens_equal '1E-1', %w[Literal.Number.Float 1E-1] + assert_tokens_equal '1E1L', %w[Literal.Number.Integer 1E1L] + assert_tokens_equal '1e1L', %w[Literal.Number.Integer 1e1L] + end + + it 'recognizes byte array literals' do + assert_tokens_equal 'x"deadbeef"', %w[Literal.String.Other x"deadbeef"] + end + + it 'highlights function names' do + tokens = subject.lex('function my_func() {}') + assert { tokens.any? { |t, v| t.qualname == 'Name.Function' && v == 'my_func' } } + end + + it 'recognizes attribute access' do + tokens = subject.lex('user.name') + assert { tokens.any? { |t, v| t.qualname == 'Name.Attribute' && v == 'name' } } + end + end +end diff --git a/spec/visual/samples/rell b/spec/visual/samples/rell new file mode 100644 index 0000000000..a49172f1b4 --- /dev/null +++ b/spec/visual/samples/rell @@ -0,0 +1,246 @@ +/* + * Rell Language Visual Sample + * Comprehensive demonstration of Rell syntax + */ + +// Module declaration with annotation +@mount("my_module") +module; + +// Namespace declaration +namespace models { + + // Entity definition with keys and indices + entity user { + key username: text; + mutable email: text; + mutable balance: integer = 0; + index email; + } + + // Object definition + object configuration { + settings: json = json("[]"); + version: integer = 1; + } + + // Struct definition + struct user_profile { + full_name: text; + age: integer; + verified: boolean; + } + + // Enum definition + enum status { + ACTIVE, + INACTIVE, + SUSPENDED + } +} + +// Function with various parameter types +function calculate_total( + amount: integer, + rate: decimal, + enabled: boolean = true +): decimal { + if (enabled) { + return amount * rate; + } else { + return amount.to_decimal(); + } +} + +// Function with nullable return type +function find_by_id(id: rowid): models.user? { + return models.user @? { .rowid == id }; +} + +// Query definition with at-expressions +query get_active_users(min_balance: integer = 0) { + // At-expression with filtering + return models.user @* { .balance >= min_balance } ( + $.username, + .balance + ); +} + +// Query with limit and offset +query get_users_paginated(page: integer, page_size: integer) { + return models.user @* {} limit page_size offset (page * page_size); +} + +// Operation with create statement +@singular() +operation register_user( + username: text, + email: text, + initial_balance: integer = 100 +) { + require(username.size() > 0, "Username cannot be empty"); + + // Create new entity + create models.user( + username, + email, + balance = initial_balance + ); +} + +// Operation with update statement +operation update_email(username: text, new_email: text) { + // Update with at-expression + update models.user @? { .username == username } ( + email = new_email + ); +} + +// Operation with delete statement +operation delete_user(username: text) { + delete models.user @ { .username == username }; +} + +// Operation demonstrating control flow +operation process_transaction( + username: text, + amount: integer, + transaction_type: text +) { + val user = models.user @ { .username == username }; + + // When expression + when (transaction_type) { + "deposit" -> { + user.balance += amount; + } + "withdraw" -> { + require(user.balance >= amount, "Insufficient balance"); + user.balance -= amount; + } + else -> { + require(false, "Invalid transaction type"); + } + } +} + +// Function with guard clause +operation validate_amount(amount: integer) { + guard { + require(amount > 0); + } +} + +// Demonstrating various operators and literals +function operator_examples() { + // Boolean literals + val is_active = true; + val is_deleted = false; + + // Null literal + val optional_value: text? = null; + + // Integer literals + val count = 42; + val hex_value = 0xDEADBEEF; + val big_int = 123456789L; + + // Decimal literals + val price = 19.99; + val scientific = 1.5e10; + val negative_exp = 2.0e-5; + + // String literal with escapes + val message = "Hello, \"World\"!\nNew line\tTab"; + + // Byte array literal + val data = x"DEADBEEF"; + + // Comparison operators + if (count == 42 and price != 0.0) { + val result = count > 10 or price < 100.0; + } + + // Reference equality + if (optional_value == null or optional_value != null) { + // Do something + } + + // Arithmetic operators + var total = count + 10; + total -= 5; + total *= 2; + total /= 3; + total %= 7; + total++; + total--; + + // Null safety operators + val safe_call = optional_value?.size(); + val elvis_op = optional_value ?: "default"; + val not_null = optional_value!!; + val null_check = optional_value??; +} + +// For loop +function iterate_users() { + val users = models.user @* {}; + for (u in users) { + // Process user + if (u.balance < 0) { + continue; + } + if (u.balance > 10000) { + break; + } + } +} + +// While loop +function count_down(start: integer) { + var counter = start; + while (counter > 0) { + counter--; + } +} + +entity base_entity { + key id: integer; +} + +entity derived_entity { + mutable extra_field: text; +} + +struct foo { x: virtual>; } + +// Complex query with multiple at-expressions +query get_user_summary(username: text) { + val u = models.user @ { .username == username }; + val recent_count = models.user @* { .balance > 100 } ( .username ); + + return ( + username = u.username, + balance = u.balance, + total_active = recent_count.size() + ); +} + +// Demonstrating tuple and list operations +function collection_examples() { + // Tuple + val tuple_val = (name = "Alice", age = 30); + + // List + val numbers = [1, 2, 3, 4, 5]; + val first = numbers[0]; + + // Map + val mapping = ["key1": 100, "key2": 200]; + val value = mapping["key1"]; + + // Set + val unique_items = set([1, 2, 2, 3, 3, 3]); + + return numbers.size() + unique_items.size(); +}