Lyric Language Specification
Version 1.1.0, March 2026
Table of Contents
- 1. Overview
- 2. Syntax Design Philosophy
- 3. Syntax Summary
- 4. Basic Structure
- 5. Core Constructs
- 6. Type System
- 7. Classes and Inheritance
- 8. Data Structures
- 9. Modules and Imports
- 10. File I/O
- 11. Command-Line and Shell
- 12. Error Handling
- 13. Regular Expressions
- 14. Shebang Support
- 15. Built-in Functions
- 16. Standard Library
- 17. Current Scope of Lyric
1. Overview
Lyric is a modern, beginner-friendly programming language designed for simplicity, readability, and teaching. 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, inheritance with access control, file I/O, shell integration, and built-in data structures with comprehensive error handling and performance optimizations.
1.1 Execution Model
Lyric uses a dual execution architecture with two backends that produce identical results for all valid programs:
Bytecode Compiler (default) β Lyric source is parsed into an AST, then transpiled to Python
astmodule nodes and compiled to CPython bytecode viacompile(). The compiled bytecode is executed withexec(), achieving near-native Python performance. This is a transpiler β Lyric AST nodes are mapped to equivalent Python AST constructs, with a runtime support library (compiled_runtime) handling Lyric-specific semantics like typed assignments, string auto-coercion, and file I/O operators.Tree-Walking Interpreter (fallback) β The original interpreter walks the Lyric AST directly, evaluating each node recursively with lexical scope managed through linked environment frames. This mode is available via the
--interpretflag and is used automatically by the REPL (lyric -i).
Default (compiled):
Source (.ly) β Lexer β Parser β Lyric AST β Compiler β Python AST β compile() β exec()
With --interpret:
Source (.ly) β Lexer β Parser β Lyric AST β Tree-Walking Interpreter
The bytecode compiler is the default because it provides significantly better performance for compute-heavy workloads (20-50x speedup for loops and function calls). The tree-walking interpreter remains available as a reference implementation and fallback.
The interactive REPL (lyric -i) always uses the tree-walking interpreter, as it is better suited for incremental evaluation of individual expressions and statements with persistent state across inputs.
2. Syntax 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.
3. Syntax Summary
| Construct | Summary |
|---|---|
| Conditional | Opens with if, elif (or case), or else; closes with end. Python-style conditionals. |
| For Loop | for type var in ... iterates over a range or collection with inline variable declaration, closes with done. Supports break and continue. |
| Given Loop | given condition: repeats while the condition is True (while loop), 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 and inheritance with based on. |
| Inheritance | Single inheritance using class Child based on Parent: syntax. Automatic constructor chaining. |
| Access Modifiers | Methods support public, private, protected keywords. Attributes remain public. |
| 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. importpy enforces a two-tier blacklist: module-level (26 blocked modules) and attribute-level (9 blocked sys attributes). |
| Entry Point | Always begins with def main() or def main(int argc, arr argv) for command-line arguments. |
| 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, tup, map, obj, dsk, pyobject, and var. |
| File I/O | dsk type with 11 methods. File operators: ->> (append), -> (write), <- (read). |
| Shell Integration | exec() for commands, exit() for termination. Chain operators: ` |
| 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. |
4. Basic Structure
Every Lyric program starts execution in a main function.
There is no __name__ or hidden runtime boilerplate.
def main() {
print("Hello, world!")
}
5. Core Constructs
5.1 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
5.2 For Loops
for type var in ...iterates over a range, array, tuple, map, or any other collection- The loop variable can be declared inline with its type:
for int i in range(5) - The loop variable automatically receives the next value on each iteration
- Block introduced by optional
:and closed bydone - Supports
break(exit loop) andcontinue(skip to next iteration)
# Iterate over a range of numbers
for int i in range(5):
print(i)
done
# Iterate over an array
for str color in ["red", "green", "blue"]:
print(color)
done
5.3 Given Loops (While)
given condition:repeats the body while the condition evaluates toTrue- The condition is checked before each iteration β if it is
Falsefrom the start, the body never runs - The programmer must ensure something inside the body eventually makes the condition
False, otherwise the loop runs forever - Block introduced by optional
:and closed bydone - Supports
break(exit loop) andcontinue(skip to next iteration)
# While-style loop
given n > 0:
print(n)
n -= 1
done
5.4 Break and Continue
for int 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 for and given loops. They are implemented as control flow signals (not exceptions) and cannot be caught by try/catch blocks.
5.5 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.
Note: Lyric does not support keyword arguments. All function arguments are positional only.
5.5.1 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
}
5.5.2 Untyped Functions
# Simple function with no return type (uses def)
def greet(name) {
print("Hello,", name)
}
def calculate(x) {
return x * 2
}
5.5.3 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
5.5.4 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
}
5.5.5 Untyped Functions
# Simple function with no return type
def greet(name) {
print("Hello,", name)
}
def calculate(x) {
return x * 2
}
5.5.6 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
6. Type System
Lyric 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 with methods | 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] |
tup |
Immutable tuples | tup coords = (10, 20) |
map |
Dictionaries with methods | map data = {"key": "value"} |
obj |
Class instance references | obj person = Person("Alice") |
dsk |
File handle for I/O operations | dsk file = disk("data.txt") |
pyobject |
Python object references | pyobject obj = some_python_object |
var |
Dynamic type (can change) | var x = 10 |
6.1 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: Types are validated at runtime;
varallows type changes
6.2 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
6.3 Variable Declaration and Assignment
Lyric identifies variable type through explicit declarations. All variables must be declared before use, preventing implicit variable creation or shadowing.
Static typing in Lyric is achieved by declaring a variable with a specific type (int, str, bin, var, etc.). Once declared, a variableβs type cannot change. Lyric currently enforces type correctness at runtime; an experimental branch is evaluating whether type checking may optionally occur at parse time before program execution, including the associated performance trade-offs.
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
6.3.1 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)
Class Instance Type (obj):
obj person = Person("Alice") # Must hold a class instance
obj account = BankAccount(0) # Type error if assigned a non-instance value
obj enforces that the variable holds a Lyric class instance. Assigning a plain
value (e.g. an integer or string) to an obj variable raises a runtime type error.
var remains valid for class instances when dynamic typing is needed.
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
6.3.2 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 cannot change at runtime
- Dynamic Flexibility:
varvariables can change type during reassignment
6.3.3 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
}
6.3.4 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")
}
6.4 Compound Assignment Operators
Compound assignment operators combine an arithmetic operation with assignment into a single step. x += 5 is equivalent to x = x + 5.
| Operator | Equivalent | Description |
|---|---|---|
+= |
x = x + expr |
Add and assign |
-= |
x = x - expr |
Subtract and assign |
*= |
x = x * expr |
Multiply and assign |
/= |
x = x / expr |
Divide and assign |
%= |
x = x % expr |
Modulo and assign |
6.4.1 Type Compatibility
| Operator | int |
flt |
str |
|---|---|---|---|
+= |
Yes | Yes | Yes (concatenation) |
-= |
Yes | Yes | TypeError |
*= |
Yes | Yes | Yes (repetition: str *= int) |
/= |
Yes (returns flt) | Yes | TypeError |
%= |
Yes | TypeError | TypeError |
Notes:
+=onstrperforms string concatenation. If either operand is astr, the other is coerced tostrautomatically.*=onstrperforms string repetition ("ab" *= 3produces"ababab")./=always produces afltresult, even withintoperands. This means using/=on a variable declared asintwill raise aTypeErrorat runtime because thefltresult cannot be stored in anintcontainer. Usefltorvarfor variables that will receive/=.%=requires both operands to beint. Usingfltorstrraises aTypeError.- Division by zero with
/=raises aRuntimeError. Modulo by zero with%=raises aZeroDivisionError.
6.4.2 Assignment Targets
Compound assignment works on all standard assignment targets:
# Simple variables
int x = 10
x += 5 # x is now 15
# Indexed access (arrays and maps)
arr scores = [10, 20, 30]
scores[0] += 5 # scores[0] is now 15
Member access (object fields):
class Counter:
def Counter() {
self.count = 0
}
def increment() {
self.count += 1
}
+++
6.4.3 Common Patterns
# Accumulator in a loop
int total = 0
var i
for i in [1, 2, 3, 4, 5]
total += i
done
# total is 15
# String building
str result = ""
var word
for word in ["hello", " ", "world"]
result += word
done
# result is "hello world"
7. Classes and Inheritance
7.1 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
}
+++
7.2 Inheritance
Lyric supports single inheritance using the based on syntax. Subclasses inherit all methods and attributes from their base class.
7.2.1 Syntax
class Parent:
var x
def Parent() {
self.x = 10
}
+++
class Child based on Parent:
var y
def Child() {
self.y = 20
}
+++
7.2.2 Constructor Chaining
Constructors are automatically chained from base to child when inheritance is used. If a class has no constructor, it is skipped.
class A:
def A() { print("A") }
+++
class B based on A:
def B() { print("B") }
+++
class C based on B:
def C() { print("C") }
+++
var obj = C() # Output: A, B, C
7.2.3 Method Overriding
Child classes can override parent methods by using the same name. The child method always takes precedence.
class Animal:
def speak() { print("Some sound") }
+++
class Dog based on Animal:
def speak() { print("Woof!") }
+++
var pet = Dog()
pet.speak() # Output: Woof!
7.3 Access Modifiers
Methods can use public, private, or protected modifiers. Attributes remain public.
public(default): Accessible from anywhereprivate: Accessible only from within the same classprotected: Accessible from the class and its subclasses
class Person:
var name
public def speak() {
print("Hello from", self.name)
}
private def think() {
print("(thinking)")
}
protected def internal_method() {
print("Protected operation")
}
+++
8. 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"])
}
8.1 str Type β String Methods
The str type represents text strings in Lyric. Strings are immutable β all methods return a new string rather than modifying the original. You must reassign the result if you want to keep it (e.g., s = s.upper()).
Declaration:
str name = "Alice"
str empty = ""
str greeting = "Hello, " + name
Case Methods:
upper()β Return string converted to uppercaselower()β Return string converted to lowercasetitle()β Return string with first letter of each word capitalizedcapitalize()β Return string with first character capitalizedswapcase()β Return string with case of each character swapped
Search Methods:
find(sub)β Return index of first occurrence, or-1if not found. Optional:find(sub, start)orfind(sub, start, end)rfind(sub)β Return index of last occurrence, or-1if not found. Optional:rfind(sub, start)orrfind(sub, start, end)index(sub)β Likefind(), but raises error if not found. Optional:index(sub, start)orindex(sub, start, end)count(sub)β Return number of non-overlapping occurrencesstartswith(prefix)β ReturnTrueif string starts with prefixendswith(suffix)β ReturnTrueif string ends with suffix
Modification Methods:
replace(old, new)β Return string with all occurrences ofoldreplaced bynewstrip()β Return string with leading and trailing whitespace removed. Optional:strip(chars)to strip specific characterslstrip()β Return string with leading whitespace removed. Optional:lstrip(chars)rstrip()β Return string with trailing whitespace removed. Optional:rstrip(chars)split()β Split string into anarrby whitespace. Optional:split(sep)to split by a specific separatorjoin(arr)β Join array elements into a string using this string as separator
Formatting Methods:
format(args...)β Return formatted string with{}placeholders replaced by argumentscenter(width)β Return string centered in a field of given width. Optional:center(width, fillchar)ljust(width)β Return string left-justified in a field of given width. Optional:ljust(width, fillchar)rjust(width)β Return string right-justified in a field of given width. Optional:rjust(width, fillchar)zfill(width)β Return string padded with leading zeros to given width
Test Methods:
isdigit()β ReturnTrueif all characters are digitsisalpha()β ReturnTrueif all characters are alphabeticisalnum()β ReturnTrueif all characters are alphanumericisspace()β ReturnTrueif all characters are whitespaceisupper()β ReturnTrueif all cased characters are uppercaseislower()β ReturnTrueif all cased characters are lowercase
Example:
str text = " Hello, World! "
str trimmed = text.strip() # "Hello, World!"
print(trimmed.upper()) # "HELLO, WORLD!"
print(trimmed.lower()) # "hello, world!"
print(trimmed.replace("World", "Lyric")) # "Hello, Lyric!"
str csv = "a,b,c"
arr parts = csv.split(",") # ["a", "b", "c"]
print(",".join(parts)) # "a,b,c"
str name = "alice"
print(name.startswith("al")) # True
print(name.find("ic")) # 2
print(name.count("a")) # 1
str padded = "42".zfill(5) # "00042"
str centered = "hi".center(10, "*") # "****hi****"
8.2 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()β Remove and return last element. Optional:pop(index)to remove at specific indexremove(value)β Remove first occurrence of valuereverse()β Reverse the list in placesort()β Sort the list in place (ascending order)
Query Methods:
len()β Return number of elementscount(value)β Count occurrences of valueindex(value)β Find index of first occurrence. Optional:index(value, start)orindex(value, start, end)copy()β 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
8.3 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)β Remove and return value for key. Optional:pop(key, default)to return a default instead of raising an errorpopitem()β Remove and return last (key, value) pair as arrsetdefault(key)β Get value for key, or set toNoneif missing. Optional:setdefault(key, default)update(other_map)β Update with key-value pairs from another map
Query Methods:
len()β Return number of key-value pairsget(key)β Get value or returnNoneif key missing. Optional:get(key, default)to return a specific defaultkeys()β Return arr of all keysvalues()β Return arr of all valuesitems()β Return arr of (key, value) pairscopy()β Return a shallow copysorted()β Return sorted arr of keys. Optional:sorted(true)for reverse order
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
8.4 tup Type β Immutable Tuples
The tup type represents fixed-length immutable sequences. Once created, a tuple's contents cannot be changed. This makes tup useful whenever you need to guarantee a sequence will not be modified, or when passing sequences to Python APIs that require native Python tuples.
Declaration:
tup coords = (10, 20)
tup mixed = (1, "hello", 3.14)
tup empty = ()
tup single = (42,) # Trailing comma required for single-element tuples
Built-in conversion:
arr items = [1, 2, 3]
tup t = tup(items) # Convert an arr to a tup
Indexing and iteration:
tup t = (10, 11, 12)
print(t[0]) # 10
var item
for item in t:
print(item)
done
if 11 in t:
print("found")
end
Read-only methods:
len()β Return number of elementscount(val)β Count occurrences ofvalindex(val)β Return index of first occurrence ofvalmin()β Return smallest elementmax()β Return largest elementsum()β Return sum of numeric elements
Mutation methods (append, remove, sort, etc.) do not exist on tup. Attempting to modify a tuple raises a TypeError.
Python interop: tup values are passed to Python as native tuple objects, which is required by Python APIs such as HTTPServer(("0.0.0.0", 8080), handler).
Example:
tup point = (3, 7)
print(point.len()) # 2
print(point.min()) # 3
print(point.max()) # 7
tup words = ("apple", "banana", "apple")
print(words.count("apple")) # 2
print(words.index("banana")) # 1
9. Modules and Imports
9.1 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.
9.1.1 Whole-Module Import (Namespace Access)
When you write import utils, Lyric creates a module namespace object bound to the name utils. All names from the module are accessed via dot notation: utils.func(), utils.VAR. Nothing is injected directly into the calling scope.
# Whole-module import β access everything through the namespace
import utils
def main() {
int result = utils.calculate(10)
obj person = utils.Person("Alice")
print(utils.PI)
print(result)
}
9.1.2 Selective Import Syntax
Import specific names using a semicolon separator. The listed names are bound directly into the calling scope β no module. prefix needed.
# Selective import β bind specific names directly into scope
import utils; calculate, format_name, Person
def main() {
int result = calculate(10) # Direct access
str 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)
9.2 Python Library Import
importpy bridges Lyric programs into Python's standard library and any installed third-party packages. Access is mediated through a PyModuleProxy object that intercepts attribute lookups, enforces the blacklist, and forwards safe accesses directly to the underlying Python module.
9.2.1 Whole-Module Import
The original syntax imports a module under its name. All attributes are accessed via dot notation.
importpy random
def main() {
print(random.randint(1, 10))
}
9.2.2 Selective Import Syntax
A semicolon after the module name, followed by a comma-separated list of names, binds those names directly into the calling scope. This mirrors Python's from module import Name1, Name2. Both functions and classes are supported. Dotted module names (e.g., http.server) are also supported.
# Bind sqrt and pi directly into scope
importpy math; sqrt, pi
def main() {
print(sqrt(16.0)) # 4.0
print(pi) # 3.141592653589793
}
# Selective import from a dotted module
importpy http.server; HTTPServer, SimpleHTTPRequestHandler
def main() {
tup addr = ("0.0.0.0", 8080)
var server = HTTPServer(addr, SimpleHTTPRequestHandler)
server.serve_forever()
}
# Mixing whole-module and selective imports
importpy os
importpy functools; partial
def main() {
str cwd = os.getcwd()
print(cwd)
}
All blacklist and whitelist rules apply equally to both import forms. A blacklisted module cannot be imported selectively any more than it can be imported whole.
9.2.3 Whitelisted Modules
The following modules have been reviewed and vetted to ensure they align with Lyric's runtime model. They may be imported directly with importpy without any additional flags.
| Module | Description |
|---|---|
collections |
Specialized container types (see vet results below) |
datetime |
Date and time utilities |
http.server |
Simple HTTP server |
json |
JSON encoding and decoding |
math |
Mathematical functions and constants |
os |
Operating system interface |
random |
Random number generation |
requests |
HTTP requests (third-party) |
sys |
System-specific parameters (partially restricted β see Tier 2 blacklist) |
time |
Time access and conversions |
Modules not on the whitelist are blocked by default. Modules that are not explicitly blacklisted may be imported by running Lyric with the --unsafe flag. This flag enables broader Python interoperability and is intended for advanced users who understand the implications of stepping outside Lyric's controlled environment.
In a future version of Lyric, the use of importpy may require explicitly enabling a --interop flag. This would make Python interoperability an intentional opt-in feature, clearly separated from Lyric's core standard environment.
9.2.4 Collections Module Vet Results
The collections module has been vetted with 50/50 tests passing. The following types and operations are supported:
| Type | Supported Operations |
|---|---|
Counter |
Creation, element access, missing key (returns 0), most_common, update, clear, keys, values, total |
defaultdict |
Creation, explicit assignment, compound assignment |
deque |
Creation, append, appendleft, pop, popleft, rotate, extend, extendleft, maxlen, len, clear, count, reverse |
OrderedDict |
Creation, insertion order, move_to_end |
namedtuple |
Creation, field access, index access |
ChainMap |
Creation, lookup (override + fallthrough), keys |
Known limitation: defaultdict auto-creation on missing keys does not work. Python-to-Lyric callback is not supported, so accessing a missing key will not trigger the default factory. Use explicit assignment instead.
9.2.5 Security Blacklist
Because Lyric runs on top of the CPython interpreter, an unrestricted importpy would allow Lyric programs to reach directly into CPython's internals β inspecting live stack frames, installing bytecode trace hooks, serializing and deserializing arbitrary Python objects, calling the raw compiler, or manipulating the garbage collector. This would undermine Lyric's runtime isolation, break the language's own error model, and open avenues for privilege escalation in shared or sandboxed environments.
The blacklist is enforced in two tiers:
Tier 1 β Module-level blacklist. These modules are rejected entirely at import time. Attempting importpy <name> raises a RuntimeErrorLyric immediately, before any module code is loaded.
| Module | Reason |
|---|---|
pdb |
Interactive CPython debugger; installs frame-stepping hooks |
trace |
Line-by-line CPython bytecode tracer |
traceback |
Formats and walks live CPython stack frames |
faulthandler |
Registers low-level signal/fault handlers tied to CPython internals |
inspect |
Introspects live frames, bytecode, and source; principal frame-inspection API |
dis |
CPython bytecode disassembler; requires direct access to CodeType objects |
marshal |
Serialises/deserialises CPython bytecode and CodeType objects |
pickle |
Serialises arbitrary Python object graphs including code objects; arbitrary code execution vector |
pickletools |
Annotated pickle disassembler; same risk surface as pickle |
copyreg |
Registers custom pickle reduction functions; extends pickle's attack surface |
code |
Creates interactive CPython interpreter sessions |
codeop |
Compiles partial Python source into CodeType objects |
compile |
Direct access to the CPython compile built-in |
eval |
Direct access to the CPython eval built-in |
exec |
Direct access to the CPython exec built-in |
types |
Exposes CodeType, FrameType, FunctionType, and other CPython-internal type constructors |
importlib._bootstrap |
Private CPython import machinery bootstrap |
importlib._bootstrap_external |
Private CPython external import path handler |
zipimport |
Low-level CPython zip-archive importer |
runpy |
Executes Python modules and scripts as __main__; arbitrary code execution |
modulefinder |
Traces CPython's import graph by inspecting bytecode |
gc |
Direct access to CPython's reference-counting garbage collector |
weakref |
Exposes CPython object identity and finalization hooks |
ctypes |
Calls arbitrary native C code; complete process memory access |
cffi |
Alternative native C FFI; same risk as ctypes |
_ctypes |
CPython's internal C extension backing ctypes |
mmap |
Raw memory-mapped file access; process memory exposure |
Tier 2 β Attribute-level blacklist. Some modules (currently sys) are partially useful to Lyric programs (e.g., sys.argv, sys.platform) but carry individual attributes that reach into CPython internals. These attributes are blocked at access time; the module itself may still be imported.
Blocked sys attributes:
| Attribute | Reason |
|---|---|
settrace |
Installs a CPython bytecode trace hook on the current thread; used by debuggers and coverage tools to intercept every opcode |
setprofile |
Installs a CPython call/return profile hook; exposes the entire call stack |
gettrace |
Reads back the currently installed trace hook; blocked for symmetry with settrace |
getprofile |
Reads back the currently installed profile hook; blocked for symmetry with setprofile |
_getframe |
Returns a live FrameType object for an arbitrary depth in the call stack; most direct route into Lyric's own interpreter state |
_current_frames |
Returns all live frames across all threads; same risk as _getframe at process scope |
getrefcount |
Exposes CPython's reference count for a Python object, leaking object identity and memory layout |
addaudithook |
Installs a permanent, process-wide, non-removable audit hook that can monitor every Python operation for the lifetime of the process |
exc_info |
Returns (type, value, traceback) where the traceback object holds direct references to live CPython frame objects |
9.2.6 Error Messages
When a blacklisted module or attribute is accessed, Lyric raises a RuntimeErrorLyric with a message of the form:
ImportError: Python module 'pickle' is blacklisted in Lyric.
This module relies on CPython bytecode, frames, object identity, or interpreter internals and does not work with Lyric.
AttributeError: 'sys._getframe' is blacklisted in Lyric.
This module relies on CPython bytecode, frames, object identity, or interpreter internals and does not work with Lyric.
The message is intentionally informative so that users understand the architectural reason for the restriction rather than seeing an opaque import failure.
9.2.7 Listing Whitelisted and Blacklisted Modules
You can view the current whitelist and blacklist from the command line:
lyric --whitelist
Prints the list of all whitelisted modules that may be imported directly with importpy.
lyric --blacklist
Prints the list of all blacklisted modules that are blocked under all circumstances.
10. File I/O
10.1 The dsk Type
The dsk type provides comprehensive file operations. Files are created using the built-in disk() function.
10.1.1 Declaration
dsk myfile = disk("data.txt")
10.1.2 File Methods
| Method | Description |
|---|---|
open(mode='r') |
Open file with mode ('r', 'w', 'a', 'r+', etc.) |
close() |
Close file handle |
read() |
Read entire file as string |
readlines() |
Read all lines as arr |
write(content) |
Write content (overwrites file) |
append(content) |
Append content to file |
exists() |
Check if file exists (returns boolean) |
size() |
Get file size in bytes |
delete() |
Delete the file |
copy(toPath) |
Copy file to new path |
move(toPath) |
Move file to new path |
10.1.3 Example
def main() {
dsk logfile = disk("app.log")
logfile.write("Application started\n")
logfile.append("User logged in\n")
logfile.close()
if logfile.exists()
print("Log size:", logfile.size(), "bytes")
end
}
10.2 File Operators
Lyric provides intuitive operators for file I/O:
->>β Append data to file->β Overwrite file with data<-β Read data from file
These operators work with str, arr, int, flt, god, and map types.
The print statement can also be used with file operators to write with an automatic newline β similar to how print adds a newline on stdout:
print "text" ->> myfile # Appends "text\n" to file
print "text" -> myfile # Overwrites file with "text\n"
This is useful for building multi-line files where each line needs a newline terminator.
10.2.1 Examples
def main() {
dsk myfile = disk("data.txt")
# Append string to file (no newline)
str text = "Hello, World!"
text ->> myfile
# Append with newline using print
print "Hello, World!" ->> myfile
# Overwrite file
str new_data = "New content"
new_data -> myfile
# Read from file
str content
content <- myfile
print(content)
# Array operations
arr lines = ["line1", "line2", "line3"]
lines ->> myfile # Each element on new line
arr file_lines
file_lines <- myfile # Reads lines into array
# Build a multi-line file with print
dsk journal = disk("post.md")
print "---" ->> journal
print "title: My Post" ->> journal
print "---" ->> journal
print "" ->> journal
print "Content here." ->> journal
journal.close()
}
11. Command-Line and Shell
11.1 Command-Line Interface
11.1.1 Execution Modes
By default, Lyric uses the bytecode compiler to execute programs. The following flags control execution behavior:
lyric script.ly # Run with bytecode compiler (default)
lyric run script.ly # Same as above
lyric script.ly --interpret # Use tree-walking interpreter instead
lyric script.ly --dump-ast # Print the generated Python AST and exit
lyric -i # Start REPL (uses interpreter mode)
lyric -i "print(2 + 2)" # Execute immediate code (uses interpreter mode)
| Flag | Description |
|---|---|
| (none) | Bytecode compiled execution (default) |
--interpret |
Use the tree-walking interpreter instead of the bytecode compiler |
--dump-ast |
Print the generated Python AST without executing (useful for debugging the compiler) |
--unsafe |
Allow importpy of non-whitelisted modules (blacklisted modules are still blocked) |
The LYRIC_INTERPRET environment variable can also be set to force interpreter mode without passing the flag.
The REPL (lyric -i) and immediate execution (lyric -i "code") always use the tree-walking interpreter regardless of flags, as interpreter mode is better suited for incremental, stateful evaluation.
11.1.2 Script Arguments
The main() function can accept arguments passed from the command line.
def main(int argc, arr argv) {
print("Argument count:", argc)
for str arg in argv
print("Arg:", arg)
done
}
Run: lyric script.ly arg1 arg2 arg3
11.1.2 Option Parsing
The getopts() function retrieves command-line options. It always takes exactly two arguments: getopts(short, long) where the first is the short option name and the second is the long option name. Use None if you only need one form.
def main() {
god verbose = getopts("v", "verbose") # -v or --verbose
god debug = getopts("d", None) # -d only
var file = getopts(None, "file") # --file=path only
god help = getopts("h", "help") # -h or --help
if help
print("Usage: script.ly [options]")
exit()
end
if verbose
print("Verbose mode enabled")
end
if file != false
print("Processing:", file)
end
}
Run: lyric script.ly -d --verbose --file=data.txt
Rules:
getopts(short, long)β always two arguments, either can beNone- Single-character options use
-(e.g.,-a,-v) - Multi-character short options are supported (e.g.,
-hcforgetopts("hc", "high-capacity")) - Long options use
--(e.g.,--verbose,--debug) - Options with values use
=(e.g.,--file=data.txt,-o=output.txt) - Missing options return
false - Flags return
truewhen present
11.2 Shell Integration
11.2.1 Program Termination
The exit() function terminates the program with an exit code.
def main() {
if error_condition
exit(1) # Exit with error code
end
exit(0) # Exit successfully (default)
}
11.2.2 Command Execution
The exec() function executes a shell command via the system shell and returns the command's exit code as an int. The command runs synchronously; exec() does not return until the process exits.
def main() {
int rc = exec("ls -la")
print("Command returned:", rc)
}
11.2.3 stderr Merge Contract
In all exec capturing paths, stderr is merged into stdout. A command's output is defined as the union of its standard output and standard error streams. This is a language-level guarantee, not a shell-level convention. There is no mechanism to capture stdout and stderr separately.
This contract applies uniformly across all four exec paths:
| Path | Syntax example | Behaviour |
|---|---|---|
| Return-code only | exec("cmd") |
stdout + stderr printed live; exit code returned |
| Output capture | exec("cmd") -> var |
stdout + stderr captured into var |
| File append | exec("cmd") ->> file |
stdout + stderr appended to dsk file |
| Pipeline stage | exec("cmd1") | exec("cmd2") |
stdout + stderr of each stage are forwarded to the next |
The rationale is that command-line tools frequently write diagnostic information, progress messages, or error details to stderr. Silently discarding stderr would cause programs to lose critical output that a user would expect to see or capture. Merging the streams gives Lyric programs a simple, predictable model: whatever a command prints, you get.
11.2.4 I/O Redirection
Commands can redirect output to variables or files:
def main() {
# Capture stdout + stderr into a variable
str output
exec("echo Hello") -> output
print("Got:", output)
# Capture stderr-only commands (stderr is merged, so this works naturally)
str err_output
exec("ls /nonexistent/path") -> err_output
print("Error output:", err_output)
# Append stdout + stderr to a file
dsk logfile = disk("output.txt")
exec("date") ->> logfile
}
11.2.5 Command Chaining
Chain commands with pipe and control flow operators:
|β Pipe output (stdout + stderr) to the next command's stdin&&β Execute next only if previous succeeded (exit code 0)||β Execute next only if previous failed (non-zero exit code)
def main() {
# Pipe commands
str result
exec("echo hello") | exec("cat") -> result
# Conditional execution
exec("make clean") && exec("make all") && exec("make test")
# Fallback execution
exec("which python3") || exec("which python") || print("Not found")
# Complex chains
int rc = exec("test -f config.txt") || exec("cp default.txt config.txt")
if rc != 0
print("Configuration setup failed")
exit(1)
end
}
Chains can be combined with I/O redirection:
# Pipe to print
exec("cat data.txt") | exec("grep ERROR") | print
# Pipe to file
dsk logs = disk("errors.log")
exec("journalctl -n 100") | exec("grep ERROR") ->> logs
12. 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
12.1 Exception Types
Errorβ generic catch-all that will catch any exception regardless of typeIndexErrorβ 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.
13. Regular Expressions
Lyric supports regular expressions using the regex() built-in function. Regex objects provide comprehensive pattern matching and text processing capabilities.
13.1 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,}")
13.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+")
13.3 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. |
13.4 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). |
13.5 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]
}
14. 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.
14.1 Supported Formats
#!/usr/bin/lyric
#!/usr/bin/env lyric
#!/usr/local/bin/lyric
14.2 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.
15. 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()).
15.1 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. |
15.1.1 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.
15.2 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. |
| tup(value) | Converts value to a tup (immutable tuple). Arrays and lists become tuples; an existing tup is returned as-is. |
| map(value) | Converts value to a map (dictionary). Arrays become indexed maps with string keys. |
15.3 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. |
15.4 File Operations
| Function | Description |
|---|---|
| disk(path) | Creates a dsk object for file operations at the specified path. Does not open the file immediately. |
| open(path) | Opens the file at the specified path and returns an iterable over its lines. Files are read-only. (Legacy function) |
15.5 Shell Operations
| Function | Description |
|---|---|
| exit(code=0) | Terminates the program with the specified exit code. Defaults to 0 (success). |
| exec(command) | Executes a shell command and returns its exit code as int. stderr is always merged into stdout across all capturing paths: -> (variable), ->> (file append), and | (pipeline). Can be used with I/O operators and chain operators (|, &&, ||). |
| getopts(short, long) | Retrieves a command-line option by short name, long name, or both. Always takes two arguments β use None for the unused slot. Examples: getopts("v", "verbose"), getopts(None, "verbose"), getopts("v", None). For flags, returns true if present. For options with values (e.g., --file=path), returns the value. Returns false if option not provided. |
15.6 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)
16. Standard Library
Lyric ships with a standard library that provides common utilities for file system operations, date and time, random numbers, and environment management. The standard library is imported using the import lyric statement and accessed via the lyric namespace. The name lyric and lyrical are reserved for the standard library implementation as it evolves.
import lyric
def main() {
str cwd = lyric.pwd()
print("Working directory:", cwd)
tup d = lyric.date()
print("Date:", d[0], "Time:", d[1])
}
The standard library has elevated import privileges β it can use any non-blacklisted Python module internally via importpy without requiring the --unsafe flag. The blacklist is still enforced to prevent access to CPython internals.
16.1 Time & Sleep
| Function | Return | Description |
|---|---|---|
| lyric.sleep(seconds) | β | Pauses execution for the given number of seconds. Accepts fractional values (e.g., 0.1 for 100ms). |
| lyric.now() | int |
Returns the current Unix timestamp as an integer (seconds since epoch). |
| lyric.date() | tup |
Returns a tuple of two strings: ("YYYY-MM-DD", "HH:MM:SS") representing the current local date and time. |
| lyric.datefmt(fmt) | str |
Returns the current local date/time formatted according to the given format string. Uses Python strftime format codes (e.g., "%Y-%m-%d", "%H:%M:%S", "%A"). |
import lyric
def main() {
lyric.sleep(0.5)
int timestamp = lyric.now()
print("Unix time:", timestamp)
tup d = lyric.date()
print("Date:", d[0])
print("Time:", d[1])
str formatted = lyric.datefmt("%B %d, %Y")
print("Today is:", formatted)
}
16.2 Random
| Function | Return | Description |
|---|---|---|
| lyric.randflt() | flt |
Returns a random floating-point number in the range [0.0, 1.0). |
| lyric.randint(low, high) | int |
Returns a random integer between low and high (inclusive on both ends). |
| lyric.randarr(list) | var |
Returns a random element from the given array. |
| lyric.seed(value) | β | Seeds the random number generator with the given integer value. Useful for reproducible results. |
import lyric
def main() {
lyric.seed(42)
flt r = lyric.randflt()
print("Random float:", r)
int n = lyric.randint(1, 100)
print("Random int:", n)
arr colors = ["red", "green", "blue"]
var pick = lyric.randarr(colors)
print("Random color:", pick)
}
16.3 File System
| Function | Return | Description |
|---|---|---|
| lyric.exists(path) | god |
Returns true if the file or directory at path exists. |
| lyric.isfile(path) | god |
Returns true if path is an existing regular file. |
| lyric.isdir(path) | god |
Returns true if path is an existing directory. |
| lyric.mkdir(path) | β | Creates a new directory at path. Raises an error if the directory already exists. |
| lyric.rmdir(path) | β | Removes the empty directory at path. Raises an error if the directory is not empty. |
| lyric.rm(path) | β | Removes the file at path. Raises an error if the file does not exist. |
| lyric.ls(pattern) | arr |
Lists directory contents. For "." and "..", returns all entries in that directory. For any other pattern, performs glob matching (e.g., "*.ly", "src/**/*.py"). |
| lyric.cd(path) | β | Changes the current working directory to path. |
| lyric.pwd() | str |
Returns the current working directory as an absolute path string. |
import lyric
def main() {
print("CWD:", lyric.pwd())
if lyric.exists("output")
print("output/ already exists")
else
lyric.mkdir("output")
print("Created output/")
end
arr files = lyric.ls("*.ly")
print("Lyric files:", files)
lyric.rmdir("output")
}
16.4 Path Utilities
| Function | Return | Description |
|---|---|---|
| lyric.join(path, file) | str |
Joins two path components using the platform's path separator. |
| lyric.path(file) | str |
Returns the absolute path for the given file or relative path. |
| lyric.base(file) | str |
Returns the base name (file name) component of a path. |
| lyric.dir(file) | str |
Returns the directory component of a path. |
import lyric
def main() {
str full = lyric.join("src", "main.ly")
print("Joined:", full)
str abs = lyric.path("main.ly")
print("Absolute:", abs)
str name = lyric.base("/home/user/project/main.ly")
print("Base:", name)
str parent = lyric.dir("/home/user/project/main.ly")
print("Dir:", parent)
}
16.5 Environment
| Function | Return | Description |
|---|---|---|
| lyric.env(name) | str |
Returns the value of the environment variable name, or None if it is not set. |
| lyric.set(name, value) | β | Sets the environment variable name to value for the current process. |
| lyric.pid() | int |
Returns the process ID of the current Lyric interpreter process. |
import lyric
def main() {
str home = lyric.env("HOME")
print("Home:", home)
lyric.set("MY_APP_MODE", "debug")
print("Mode:", lyric.env("MY_APP_MODE"))
int p = lyric.pid()
print("PID:", p)
}
17. Current Scope of Lyric
Lyric is intentionally starting with a small, focused feature set. The goal at this stage is to provide a language that is easy to read, easy to reason about, and approachable for beginners, while establishing a stable foundation that can evolve over time.
As the project matures, more advanced language features may be introduced where they clearly add value and align with Lyricβs design philosophy.
At this time, the following features are not part of the current language design:
- Comprehensions (for example, [x for x in y])
- Lambda expressions (anonymous functions)
- Decorators or function modifiers
- Context managers (with-style constructs)
- Asynchronous programming constructs (async / await)
These features are not ruled out long-term, but they are intentionally excluded from the initial design to keep the language simple and focused.
17.1 Planned Development
Standard Library Expansion
Lyric ships with a native standard library (import lyric) providing file system operations, date and time utilities, random number generation, and environment management. The standard library will continue to expand incrementally over time with additional modules covering areas such as JSON handling, networking, string utilities, and similar tasks. As it matures, reliance on importpy for common use cases will be further reduced in favor of native implementations.
