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
- Clarity before cleverness β syntax must express intent plainly.
- Simplicity over completeness β features exist only when they add clarity.
- Visual harmony β code structure and syntax designed to be logical and easy to read.
- Consistency β similar constructs end with parallel patterns (
end,done,fade,+++). - 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
- Uses
eliffor conditional chaining (Python-style) caseis also supported as an alias forelif- Block introduced by optional
:and closed byend - Both
if condition:andif conditionare valid - Note:
else ifis NOT supported (useelifinstead)
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
forandgivenare lexical aliases (completely interchangeable)- Block introduced by optional
:and closed bydone - Both
for condition:andfor conditionare valid - Supports
break(exit loop) andcontinue(skip to next iteration)
# 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:
- Typed Functions β with return type before the function name (without
def) - Untyped Functions β using only the
defkeyword
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 use syntax:
TYPE funcname()(e.g.,int add(),str format()) - Untyped functions use syntax:
def funcname()(e.g.,def helper()) - Functions with explicit return types validate returned values at runtime
- Functions without explicit types automatically infer the return type from all
returnstatements - Inconsistent return types (e.g., returning both
intandstr) produce a compile error - Functions with no
returnstatement have an inferred type ofNone - Type compatibility:
int+fltoperations returnflt; division always returnsflt
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
- Functions with explicit return types validate returned values at runtime
- Functions without explicit types automatically infer the return type from all
returnstatements - Inconsistent return types (e.g., returning both
intandstr) produce a compile error - Functions with no
returnstatement have an inferred type ofNone - Type compatibility:
int+fltoperations returnflt; division always returnsflt
# 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
+++closes a class, symbolizing addition of a structure to the program- Classes are lightweight containers for variables and functions
- Block introduced by optional
:after class name - Both
class Name:andclass Nameare valid - Constructor Recognition: A method with the same name as the class acts as a constructor
- Constructors are automatically invoked when an instance is created
- The older
init()method form is still supported for backward compatibility
# 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
- Lists and dictionaries with indexing support
- Slicing syntax:
sequence[start:end:step]for strings, lists, and tuples - Built-in functions for manipulation
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:
append(item)- Add an element to the endclear()- Remove all elementsextend(other_arr)- Add all elements from another arrinsert(index, item)- Insert element at specific indexpop(index=-1)- Remove and return element at index (default: last)remove(value)- Remove first occurrence of valuereverse()- Reverse the list in placesort(key=None, reverse=False)- Sort the list in place
Query Methods:
len()- Return number of elementscount(value)- Count occurrences of valueindex(value, start=0, end=None)- Find index of first occurrencecopy()- Return a shallow copymax()- Return largest elementmin()- Return smallest elementsum()- Return sum of numeric elements
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:
clear()- Remove all itemspop(key, default=None)- Remove and return value for keypopitem()- Remove and return last (key, value) pair as arrsetdefault(key, default=None)- Get value or set default if missingupdate(other_map)- Update with key-value pairs from another map
Query Methods:
len()- Return number of key-value pairsget(key, default=None)- Get value or return default if key missingkeys()- Return arr of all keysvalues()- Return arr of all valuesitems()- Return arr of (key, value) pairscopy()- Return a shallow copysorted(reverse=False)- Return sorted arr of keys
Static Methods:
fromkeys(keys, value=None)- Create map from keys with default value
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:
- Current working directory
- Directory where the script being executed lives
- Directories specified in
LYRIC_PATHenvironment variable
Restrictions:
- Only functions, classes, and module-level variables can be imported
- Module names must be valid Lyric identifiers
- Module-level variables must be declared at the top level (outside functions/classes)
Python Library Import
- imports a Python library
importpy random
def main() {
print(random.randint(1, 10))
}
File I/O (Planned)
givenreads naturally with iterables- Explicit and readable file handling
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.
- Block introduced by optional
:after keywords - Both
try:,catch:,finally:andtry,catch,finallyare valid - Typed catch clauses:
catch ExceptionType as variable_name - Multiple catch blocks can be specified for different exception types
- Plain
catch:acts as a fallback for any exception LyricErroris the base class for all Lyric exceptions- Try blocks are closed by
fade
Exception Types
IndexErrorβ list/string index out of rangeKeyErrorβ dictionary key not foundTypeErrorβ type mismatch in operationsValueErrorβ invalid value for operationAttributeErrorβ attribute/member not foundZeroDivisionErrorβ division by zero
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.
- Static typing is achieved by declaring a variable with a specific type (int, str, bin, var, etc.). Once declared, its type cannot change.
int count = 10
count = "ten" # TypeError
- Dynamic typing is enabled through the var keyword, which allows the variableβs type to change at runtime.
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
- Declaration Before Use: All variables must be explicitly declared before assignment or reference
- Initialization Tracking: Variables are marked as declared but uninitialized until first assignment
- Type Enforcement: Static types enforce type checking at runtime
- Dynamic Flexibility:
varvariables 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:
mylist.len()instead oflen(mylist)mylist.append(item)instead ofappend(mylist, item)mydict.keys()instead ofkeys(mydict)mydict.values()instead ofvalues(mydict)
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
- Numeric Operations:
int+intβint;int+fltβflt;flt+fltβflt - Division: Always returns
fltregardless of operand types (10 / 2β5.0) - String Operations: Concatenation with
+automatically converts non-string operands - Boolean Aliases:
godandbinare completely interchangeable - Type Inference: Functions without explicit return types infer types from return statements
- Type Enforcement: Static types are validated at runtime;
varallows type changes
Scope and Redeclaration
- Variables cannot be redeclared in the same scope
- Each function and method creates its own scope
- Variables from outer scopes are accessible but cannot be redeclared in inner scopes
- Class members have class-level scope
What is NOT Planned for Lyric
To maintain simplicity and focus on core concepts:
- No comprehensions (
[x for x in y]) - No lambdas (anonymous functions)
- No decorators (function modifiers)
- No context managers (
withstatements) - No async/await (asynchronous programming)
What is currently planned for Lyric
- Class scoping with public, private, and protected modifiers is planned for a future release.
- Class inheritance will be implemented using the based on keyword.
- Abstract classes are being considered for inclusion to support inheritance-based design patterns.
- Interface definitions may be introduced to enable contract-based polymorphism.
