Lyric Language Specification

Draft Version 0.7.0, November 2025

Overview

Lyric is a modern, beginner-friendly programming language designed for simplicity, readability, and elegance.
It blends Python's clean structure with a clear, expressive syntax that emphasizes understanding over complexity.
Lyric is intended for use in introductory computer science courses, helping students learn core programming concepts through an approachable and consistent language design.

The language is implemented in Python and currently supports core programming constructs including functions, conditionals, loops, classes, and built-in data structures with comprehensive error handling and performance optimizations.

Version 0.7.0 Summary: This version introduces comprehensive collection types and module system. Key features include: arr type for arrays/lists with 14 methods (append, clear, copy, count, extend, index, insert, pop, remove, reverse, sort, len, max, min, sum), map type for dictionaries with 13 methods (clear, copy, fromkeys, get, items, keys, pop, popitem, setdefault, update, values, len, sorted), obj type for explicit class instance typing, import statement for Lyric modules with selective imports, new function syntax (int funcname() instead of int def funcname()), bin() and god() casting functions for boolean conversion, and the in operator for membership checks. Method-based APIs replace global built-in functions for len(), append(), keys(), and values().


Design Philosophy

Readable, structured, and designed for clarity and simplicity

  1. Clarity before cleverness β€” syntax must express intent plainly.
  2. Simplicity over completeness β€” features exist only when they add clarity.
  3. Visual harmony β€” code structure and syntax designed to be logical and easy to read.
  4. Consistency β€” similar constructs end with parallel patterns (end, done, fade, +++).
  5. Braces only where needed β€” functions use braces; everything else uses natural terminators.

Syntax Summary

Construct Summary
Conditional Opens with if, elif (or case), or else; closes with end. Python-style conditionals.
Loop Opens with for or given (aliases), closes with done. Supports break and continue.
Function Defined with def funcname() or with return type int add(). Enclosed in { ... }.
Class Declared with class, closed by +++. Supports constructors matching class name.
Try / Except Structured with try, catch (with optional type), finally, closed by fade.
Import System Uses import for Lyric modules and importpy for Python bridges. Single-line syntax.
Entry Point Always begins with def main(). Acts as the mandatory starting function.
Lists Declared with [ ... ]. Built-in sequence type. Supports slicing [start:end:step].
Dictionaries Declared with { ... }. Built-in key/value data type.
Type Declarations Supports int, str, flt, rex, bin/god, arr, map, obj, pyobject, and var.
Regular Expressions Created with regex("pattern") function. Supports full regex operations.
Shebang Support Scripts may start with #!/usr/bin/lyric or #!/usr/bin/env lyric for direct execution.

Basic Structure

Every Lyric program starts execution in a main function.
There is no __name__ or hidden runtime boilerplate.

def main() {
    print("Hello, world!")
}

Core Constructs

Conditionals

if x > 0:
    print("positive")
elif x < 0:
    print("negative")
else:
    print("zero")
end

# Alternative with case (equivalent to elif)
if score > 90:
    print("A")
case score > 80:
    print("B")
case score > 70:
    print("C")
else:
    print("F")
end

Loops

# Iterator loop (for style)
for i in range(5):
    print(i)
done

# Iterator loop (given style - identical behavior)
given i in range(5):
    print(i)
done

# While-style loop
given n > 0:
    print(n)
    n -= 1
done

# Break and continue
for i in range(10):
    if i == 5:
        break  # Exit loop when i equals 5
    end
    if i == 3:
        continue  # Skip iteration when i equals 3
    end
    print(i)
done

Note: break and continue are legal only inside loops. They are implemented as control flow signals (not exceptions) and cannot be caught by try/catch blocks.

Functions

Functions can be declared in two ways:

  1. Typed Functions β€” with return type before the function name (without def)
  2. Untyped Functions β€” using only the def keyword

Important: Do not mix these forms. Choose one style for each function.

Typed Functions with Return Type

# Explicit return type (no def keyword)
str greet(str name) {
    return "Hello, " + name
}

int add(int a, int b) {
    return a + b
}

flt calculate(int x, flt y) {
    return x * y
}

Untyped Functions

# Simple function with no return type (uses def)
def greet(name) {
    print("Hello,", name)
}

def calculate(x) {
    return x * 2
}

Type Inference and Return Validation

Typed Functions with Return Type

# Explicit return type
str greet(str name) {
    return "Hello, " + name
}

int add(int a, int b) {
    return a + b
}

# Return type inference (automatically inferred from return statements)
def multiply(int a, int b) {
    return a * b  # Inferred as int
}

Untyped Functions

# Simple function with no return type
def greet(name) {
    print("Hello,", name)
}

def calculate(x) {
    return x * 2
}

Type Inference and Return Validation

# Type inference example
def abs(int n) {
    if n < 0:
        return -n  # int
    else:
        return n   # int
    end
}  # Inferred return type: int

# Type validation error example
def mystery(int n) {
    if n > 0:
        return "positive"  # str
    else:
        return 0  # int
    end
}  # CompileError: Inconsistent return types

Classes

# Constructor example (method matching class name)
class Player:
    var name
    var score
    
    def Player(str player_name) {
        self.name = player_name
        self.score = 0
        print("Player created:", self.name)
    }
    
    def greet() {
        print("Hello,", self.name)
    }
+++

var p = Player("Alice")  # Constructor called automatically
p.greet()

# Traditional init method (still supported)
class Game:
    score = 0
    
    def init() {
        self.score = 0
        print("Game initialized")
    }
    
    def update_score(points) {
        self.score += points
    }
+++

Data Structures

def main() {
    # Lists
    numbers = [1, 2, 3, 4, 5]
    print("First number:", numbers[0])
    
    # Slicing
    var text = "Hello, World!"
    print(text[0:5])    # "Hello"
    print(text[7:])     # "World!"
    print(text[:5])     # "Hello"
    print(text[::2])    # "Hlo ol!"
    
    # Dictionaries
    person = {"name": "Alice", "age": 30}
    print("Name:", person["name"])
}

arr Type - Array/List Methods

The arr type represents arrays/lists in Lyric with a comprehensive method API. All list operations are methods called on the array object.

Declaration:

arr numbers = [1, 2, 3, 4, 5]
arr names = ["Alice", "Bob", "Charlie"]
arr empty = []

Mutation Methods:

Query Methods:

Example:

arr items = [3, 1, 4, 1, 5, 9]

items.append(2)          # [3, 1, 4, 1, 5, 9, 2]
items.sort()             # [1, 1, 2, 3, 4, 5, 9]
print(items.len())       # 7
print(items.max())       # 9
print(items.count(1))    # 2

items.remove(1)          # Remove first 1
items.reverse()          # Reverse in place

map Type - Dictionary Methods

The map type represents dictionaries in Lyric with a comprehensive method API. All dictionary operations are methods called on the map object.

Declaration:

map person = {"name": "Alice", "age": 30}
map scores = {"math": 95, "english": 87}
map empty = {}

Mutation Methods:

Query Methods:

Static Methods:

Example:

map data = {"a": 1, "b": 2, "c": 3}

print(data.len())           # 3
print(data.get("a"))        # 1
print(data.get("z", 0))     # 0 (default)

arr all_keys = data.keys()     # ["a", "b", "c"]
arr all_values = data.values() # [1, 2, 3]

data["d"] = 4               # Add new key-value
data.pop("b")               # Remove key "b"

# Membership check
if "a" in data
    print("Key 'a' exists")
end

Importing Lyric Modules

Lyric supports importing functions and classes from other Lyric files using the import statement.

Important: Functions, classes, and module-level variables can be imported from Lyric modules.

Basic Import Syntax

# Import all functions and classes from utils.ly
import utils

def main() {
    result = utils.calculate(10)
    obj person = utils.Person("Alice")
    print(result)
}

Selective Import Syntax

Import specific functions or classes using semicolon separator:

# Import only specific items from utils.ly
import utils; calculate, format_name, Person

def main() {
    result = calculate(10)           # Direct access
    name = format_name("alice")      # Direct access
    obj p = Person("Bob")            # Direct access
}

Module Search Path:

Restrictions:

Python Library Import

importpy random

def main() {
        print(random.randint(1, 10))
}

File I/O (Planned)

def main() {
    total = 0
    given line in open("numbers.txt"):
        total = total + int(line.strip())
    done
    print("Total:", total)
}

Error Handling

Lyric provides comprehensive exception handling using try, catch (with optional type binding), and finally blocks.

Exception Types

def main() {
    # Simple catch
    try:
        n = int(input("Enter a number: "))
        print("Half:", n / 2)
    catch:
        print("Invalid input")
    finally:
        print("Done")
    fade
    
    # Typed catch with variable binding
    var items = [1, 2, 3]
    try:
        var x = items[10]
    catch IndexError as e:
        print("Caught IndexError:", e)
    catch KeyError as e:
        print("Caught KeyError:", e)
    catch:
        print("Caught unknown error")
    fade
}

Note: The raise keyword is reserved for future use. Currently, exceptions are raised by the runtime or through Python interop.

Regular Expressions

Lyric supports regular expressions using the regex() built-in function. Regex objects provide comprehensive pattern matching and text processing capabilities.

Syntax

# Basic regex pattern
rex pattern = regex("hello")

# Pattern with HTML tags (forward slashes don't need escaping in strings)
rex title_pattern = regex("<title>(.*?)</title>")

# Complex pattern with character classes
rex email_pattern = regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")

Escaping in String Patterns

Regex patterns are passed as strings, so standard string escaping rules apply:

# HTML tag pattern
rex div_pattern = regex("<div>(.*?)</div>")

# Pattern requiring backslash escape (for regex special chars)
rex word_boundary = regex("\\bword\\b")

# URL path pattern
rex path_pattern = regex("/api/v1/users/\\d+")

Supported Methods

Regex objects support the following methods:

Method Description
.match(text) Matches pattern at the start of text. Returns match object or None.
.search(text) Searches for pattern anywhere in text. Returns match object or None.
.findall(text) Returns a list of all non-overlapping matches in text.
.replace(text, replacement) Replaces all pattern matches with replacement string.

Match Objects

Match objects returned by .match() and .search() support group access:

Method Description
.group(0) Returns the entire matched string.
.group(n) Returns the nth captured group (1-indexed).

Examples

def main() {
    # HTML title extraction
    rex title_pattern = regex("<title>(.*?)</title>")
    var html = "<title>Hello World</title>"
    var match = title_pattern.search(html)
    if match:
        var title = match.group(1)
        print("Title:", title)  # Output: Title: Hello World
    end
    
    # Email validation
    rex email_pattern = regex("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
    var text = "Contact us at support@example.com for help"
    var emails = email_pattern.findall(text)
    print("Found emails:", emails)
    
    # Find and replace
    rex phone_pattern = regex("\\d{3}-\\d{3}-\\d{4}")
    var text = "Call 555-123-4567 or 555-987-6543"
    var result = phone_pattern.replace(text, "[REDACTED]")
    print(result)  # Output: Call [REDACTED] or [REDACTED]
}

Shebang Support

Lyric scripts may begin with a shebang line for direct execution on Unix-like systems. The shebang line is safely ignored at runtime and does not affect script execution on any platform.

Supported Formats

#!/usr/bin/lyric

#!/usr/bin/env lyric

#!/usr/local/bin/lyric

Usage

Create the file.

#!/usr/bin/lyric
def main() {
    print("Hello from executable Lyric!")
}

Run the script.

# Make executable and run
$ chmod +x hello.ly
$ ./hello.ly
Hello from executable Lyric!

Note: The shebang line is optional and is transparently removed during script parsing. Scripts work identically with or without shebangs, and on both Unix and Windows platforms.

Variable Declaration and Assignment

Lyric supports both static and dynamic typing through explicit declarations.
All variables must be declared before use, preventing implicit variable creation or shadowing.

int count = 10
count = "ten"  # TypeError
var value = 10
value = "ten"  # Allowed

Declaration Syntax

Dynamic Typing with var:

var x = 10           # Dynamic variable, can change type
var name = "Alice"   # Another var that is of str type
x = "hello"          # Type change allowed for var x variable

Static Type Declarations:

int count = 5        # Integer variable
str message = "Hi"   # String variable  
flt pi = 3.14        # Float variable
var dynamic = true   # Dynamic variable (can change type)

Boolean Types:

god flag = true      # Boolean variable (honoring Kurt GΓΆdel)
bin status = false   # Alternative boolean syntax (binary 0/1)

Note: god and bin are fully interchangeable aliases for the boolean type. Both work identically in all contexts.

Multi-Variable Declarations:

# Simple multi-declarations (same base type)
var x, y, z
int a, b, c
str name, message
# Mixed-type multi-declarations
var x, int y, str z
int count, var temp, flt pi

Declaration and Initialization Rules

  1. Declaration Before Use: All variables must be explicitly declared before assignment or reference
  2. Initialization Tracking: Variables are marked as declared but uninitialized until first assignment
  3. Type Enforcement: Static types enforce type checking at runtime
  4. Dynamic Flexibility: var variables can change type during reassignment

Error Handling

Undeclared Variable Assignment:

def main() {
    x = 10  # Error: Variable 'x' used before declaration
}

Referencing Uninitialized Variable:

def main() {
    var x    # Declared but uninitialized
    print(x) # Error: Variable 'x' referenced before assignment
    x = 10   # Now initialized
    print(x) # Works: prints 10
}

Type Mismatch:

def main() {
    int x = 10
    x = "hello"  # Error: Type mismatch: cannot assign str to variable 'x' declared as int. Expected int, but got str. Use 'var x = ...' if you need dynamic typing.
}

Invalid Type in Multi-Declaration:

def main() {
    invalid_type x, int y  # Error: Unknown type 'invalid_type' at line 1, column 1. Valid types are: int, str, flt, var
}

Function Type Declarations

# Typed function with parameters and return type
int add_numbers(int a, int b) {
    return a + b
}

# Mixed typed and untyped parameters
str format_greeting(str name, int age) {
    return "Hello " + name + ", age " + str(age)
}

# Dynamic function (no type declarations)
def greet(name) {
    print("Hello", name)
}

def main() {
    var result = add_numbers(5, 10)        # Dynamic variable
    int sum = add_numbers(20, 30)          # Static variable
    str greeting = format_greeting("Alice", 25)
    greet("World")
}

Built-in Functions

Lyric includes a set of built-in functions designed for common operations such as I/O, type conversion, iteration, and regular expressions.
These functions are available globally and mirror the behavior of their Python equivalents where appropriate.

Note: Collection operations like len(), append(), keys(), and values() are now methods on arr and map objects (e.g., mylist.len(), mydict.keys()).

I/O Functions

Function Description
print(args) Prints one or more arguments to standard output, separated by a single space. Supports both function and bare syntax.
input(prompt="") Reads a line of input from standard input, displaying an optional prompt. Returns the entered string.

Print Dual Syntax

The print statement supports two equivalent forms:

# Function-style syntax
print("Hello, world!")
print("a", "b", "c")

# Bare syntax (no parentheses)
print "Hello, world!"
print "a", "b", "c"

Both forms produce identical output and generate the same Abstract Syntax Tree (AST). They can be used interchangeably based on personal preference.

Type Conversion Functions

Function Description
int(value) Converts value to an integer. For arr and map, returns element count.
float(value) Converts value to a floating-point number. For arr and map, returns element count as float.
str(value) Converts value to its string representation.
bin(value) Converts value to boolean using truthiness rules (0, empty β†’ False; non-zero, non-empty β†’ True).
god(value) Alias for bin(). Converts value to boolean.
arr(value) Converts value to an arr (list). Strings become character lists, maps become value lists.
map(value) Converts value to a map (dictionary). Arrays become indexed maps with string keys.

Utility Functions

Function Description
range(stop) Returns a list from 0 to stop-1.
range(start, stop) Returns a list from start to stop-1.
range(start, stop, step) Returns a list from start to stop-1 by step.
type(obj) Returns the name of the object's type as a string.
isinstance(obj, class_or_type) Returns true if obj is an instance of the specified class or type.

File Operations

Function Description
open(path) Opens the file at the specified path and returns an iterable over its lines. Files are read-only in the current version.

Regular Expression Functions

Function Description
regex(pattern, flags="") Creates a regular expression object from the given pattern string. Optional flags parameter supports: i (case-insensitive), m (multiline), s (dotall), x (verbose). Returns a rex object that supports .match(), .search(), .findall(), .replace(), and .group() methods.

Note:
These built-ins provide Lyric's foundational runtime functionality.
Collection-specific operations are now methods on arr and map objects:


Type System

Lyric 0.7.0 supports the following type declarations:

Type Description Example
int Integer numbers int count = 42
flt Floating-point numbers flt pi = 3.14159
str Text strings str name = "Alice"
god / bin Boolean values (aliases) god flag = true or bin flag = false
rex Regular expression patterns rex pattern = regex("hello")
arr Arrays/lists with methods arr items = [1, 2, 3]
map Dictionaries with methods map data = {"key": "value"}
obj Class instance references obj person = Person("Alice")
pyobject Python object references pyobject obj = some_python_object
var Dynamic type (can change) var x = 10

Type Compatibility Rules

Scope and Redeclaration


What is NOT Planned for Lyric

To maintain simplicity and focus on core concepts:

What is currently planned for Lyric

← Back