Macros Reference
Complete reference for MetaScript's compile-time macro system. Macros transform code at compile-time with full type safety.
Overview
MetaScript macros run during compilation, not at runtime. This means:
- Zero runtime overhead - Generated code is optimized like hand-written code
- Full type safety - Macro output is type-checked
- Inspectable - Use
msc expandto see generated code - Debuggable - Errors point to both macro and call site
# See what macros generate
msc expand src/file.ms
# Expand specific macro
msc expand src/file.ms --macro deriveBuilt-in Macros
@derive(...traits)
Auto-generates trait implementations based on class fields.
@derive(Eq, Clone, Debug, Serialize, Hash, Default)
class User {
id: string
name: string
email: string
age: number
}Available Traits
| Trait | Generates | Method Signature |
|---|---|---|
Eq | Equality comparison | equals(other: Self): boolean |
Hash | Hash code computation | hashCode(): number |
Clone | Deep copy | clone(): Self |
Debug | Debug string | toString(): string |
Serialize | JSON serialization | toJSON(): object |
Deserialize | JSON parsing | static fromJSON(json: object): Self |
Default | Default instance | static default(): Self |
Ord | Ordering comparison | compareTo(other: Self): number |
Eq Trait
Generates field-by-field equality comparison:
@derive(Eq)
class Point {
x: number
y: number
}
// Generated:
class Point {
x: number
y: number
equals(other: Point): boolean {
return this.x === other.x
&& this.y === other.y
}
}Clone Trait
Creates deep copies of objects:
@derive(Clone)
class Config {
settings: Map<string, string>
values: number[]
}
// Generated:
class Config {
settings: Map<string, string>
values: number[]
clone(): Config {
const copy = new Config()
copy.settings = new Map(this.settings)
copy.values = [...this.values]
return copy
}
}Debug Trait
Generates human-readable debug output:
@derive(Debug)
class User {
name: string
age: number
}
const user = new User("Alice", 30)
console.log(user.toString())
// Output: User { name: "Alice", age: 30 }Serialize/Deserialize Traits
JSON serialization with type safety:
@derive(Serialize, Deserialize)
class Event {
id: string
timestamp: Date
data: object
}
// Usage
const event = new Event()
const json = event.toJSON() // { id: "...", timestamp: "...", data: {...} }
const restored = Event.fromJSON(json) // Event instance@comptime { ... }
Execute code at compile-time. Results are embedded as constants.
@comptime {
// Read file at compile time
const schema = fs.readFileSync("schema.json", "utf8")
const parsed = JSON.parse(schema)
}
// 'parsed' is now a compile-time constant
// No file read happens at runtime
console.log(parsed.version)Capabilities
| Feature | Example |
|---|---|
| File operations | fs.readFileSync(), fs.readdirSync() |
| Environment | env.get("NODE_ENV") |
| JSON/YAML parsing | JSON.parse(), YAML.parse() |
| HTTP requests | http.get() (cached) |
| Code generation | Return AST nodes |
| Math operations | All standard math |
Compile-Time Constants
@comptime {
const BUILD_TIME = new Date().toISOString()
const GIT_HASH = exec("git rev-parse HEAD").trim()
const VERSION = JSON.parse(fs.readFileSync("package.json")).version
}
console.log(`Build: ${BUILD_TIME}`) // Embedded at compile time
console.log(`Commit: ${GIT_HASH}`)
console.log(`Version: ${VERSION}`)Compile-Time Validation
@comptime {
const config = JSON.parse(fs.readFileSync("config.json"))
// Validate at compile time
if (!config.apiKey) {
throw new Error("Missing apiKey in config.json")
}
if (config.timeout < 0) {
throw new Error("timeout must be positive")
}
}@serialize(options)
Customize JSON serialization behavior.
@serialize({
rename_all: "camelCase", // Field naming convention
skip_null: true, // Omit null fields
flatten: ["metadata"], // Flatten nested objects
})
class ApiResponse {
status_code: number // Becomes "statusCode"
error_message?: string // Becomes "errorMessage", omitted if null
metadata: ResponseMeta // Fields merged into parent
}Options
| Option | Type | Description |
|---|---|---|
rename_all | "camelCase" | "snake_case" | "PascalCase" | "SCREAMING_SNAKE" | Field naming convention |
skip_null | boolean | Omit null/undefined fields |
skip_default | boolean | Omit fields with default values |
flatten | string[] | Flatten nested object fields |
deny_unknown | boolean | Error on unknown fields during deserialization |
Field-Level Options
@derive(Serialize)
class User {
@serialize.rename("user_id")
id: string
@serialize.skip
password: string
@serialize.default("anonymous")
name: string
@serialize.flatten
profile: UserProfile
}@test
Mark functions as test cases.
@test
function additionWorks(): void {
assert(add(1, 2) === 3)
}
@test("multiplication handles zero")
function multiplicationZero(): void {
assert(multiply(5, 0) === 0)
}
@test({ timeout: 5000 })
async function asyncTest(): Promise<void> {
const result = await fetchData()
assert(result.ok)
}
@test({ skip: env.CI })
function localOnlyTest(): void {
// Skipped in CI
}Test Options
| Option | Type | Description |
|---|---|---|
name | string | Custom test name |
timeout | number | Timeout in milliseconds |
skip | boolean | Skip this test |
only | boolean | Run only this test |
retry | number | Retry count on failure |
@bench
Mark functions as benchmarks.
@bench
function sortPerformance(): void {
const arr = Array.from({ length: 10000 }, () => Math.random())
arr.sort()
}
@bench({ iterations: 1000, warmup: 100 })
function hashPerformance(): void {
hash("test string")
}@deprecated(message?)
Mark declarations as deprecated with optional migration guidance.
@deprecated("Use newFunction() instead")
function oldFunction(): void {
// ...
}
@deprecated
class LegacyApi {
// ...
}Compiler warnings are generated when deprecated items are used.
Writing Custom Macros
Macros are MetaScript functions that transform AST nodes at compile time.
Basic Macro Structure
@macro
function myMacro(target: ClassDecl): ClassDecl {
// Transform and return the class
return target
}
// Usage
@myMacro
class MyClass {
// ...
}AST Node Types
| Node Type | Description | Common Use |
|---|---|---|
ClassDecl | Class declaration | Add methods, properties |
FunctionDecl | Function declaration | Wrap, modify parameters |
PropertyDecl | Property declaration | Add decorators, change type |
MethodDecl | Method declaration | Add logging, validation |
Expression | Any expression | Transform values |
Statement | Any statement | Add instrumentation |
Logging Macro Example
Add automatic logging to all methods:
@macro
function logged(target: ClassDecl): ClassDecl {
for (const method of target.methods) {
const originalBody = method.body
method.body = quote {
console.log(`Entering ${method.name}`)
const __start = performance.now()
try {
return ${originalBody}
} finally {
const __duration = performance.now() - __start
console.log(`Exiting ${method.name} (${__duration}ms)`)
}
}
}
return target
}
// Usage
@logged
class UserService {
getUser(id: string): User {
return db.findUser(id)
}
}
// Expands to:
class UserService {
getUser(id: string): User {
console.log("Entering getUser")
const __start = performance.now()
try {
return db.findUser(id)
} finally {
const __duration = performance.now() - __start
console.log(`Exiting getUser (${__duration}ms)`)
}
}
}Validation Macro Example
Generate runtime validation from types:
@macro
function validated(target: ClassDecl): ClassDecl {
// Add validate method
const validateMethod = quote {
validate(): string[] {
const errors: string[] = []
${generateValidation(target.properties)}
return errors
}
}
target.methods.push(validateMethod)
return target
}
function generateValidation(props: PropertyDecl[]): Statement[] {
return props.map(prop => {
if (prop.type === "string" && prop.annotations.includes("@email")) {
return quote {
if (!isValidEmail(this.${prop.name})) {
errors.push(`${prop.name} must be a valid email`)
}
}
}
// ... more validation rules
})
}
// Usage
@validated
class User {
@email
email: string
@range(0, 150)
age: number
}Builder Pattern Macro
Generate fluent builder API:
@macro
function builder(target: ClassDecl): ClassDecl {
const builderClass = quote {
class ${target.name}Builder {
private __instance: ${target.name} = new ${target.name}()
${target.properties.map(prop => quote {
${prop.name}(value: ${prop.type}): this {
this.__instance.${prop.name} = value
return this
}
})}
build(): ${target.name} {
return this.__instance
}
}
}
// Add static builder() method to target
target.methods.push(quote {
static builder(): ${target.name}Builder {
return new ${target.name}Builder()
}
})
return target
}
// Usage
@builder
class Config {
host: string
port: number
timeout: number
}
// Enables:
const config = Config.builder()
.host("localhost")
.port(8080)
.timeout(5000)
.build()Quasiquotation
The quote { ... } syntax creates AST nodes with interpolation.
// Create an expression
const expr = quote { 1 + 2 }
// Interpolate values with ${}
const name = "myVar"
const stmt = quote { const ${name} = 42 }
// Interpolate arrays with ...${}
const args = ["a", "b", "c"]
const call = quote { myFunction(...${args}) }
// Nested quotes
const outer = quote {
const inner = quote { nested }
}Quote Operators
| Operator | Description |
|---|---|
${expr} | Interpolate single node |
..${arr} | Splice array of nodes |
$${str} | Create identifier from string |
Macro Hygiene
MetaScript macros are hygienic by default - generated identifiers don't clash with user code.
@macro
function withTemp(target: FunctionDecl): FunctionDecl {
// 'temp' is hygienic - won't clash with user's 'temp'
target.body = quote {
const temp = computeValue()
${target.body}
cleanup(temp)
}
return target
}
// User code
@withTemp
function process(): void {
const temp = "my temp" // Different from macro's temp
console.log(temp) // Prints "my temp"
}Breaking Hygiene
Use unhygienic to intentionally share identifiers:
@macro
function defineLogger(target: ClassDecl): ClassDecl {
// 'log' is shared with user code
target.body.prepend(unhygienic(quote {
const log = createLogger(${target.name})
}))
return target
}
// User code can use 'log'
@defineLogger
class Service {
handle(): void {
log.info("handling") // Uses macro-defined 'log'
}
}Macro Debugging
Expand Macros
# Show all macro expansions
msc expand src/file.ms
# Show specific macro
msc expand src/file.ms --macro derive
# Output to file
msc expand src/file.ms -o expanded.msCompile-Time Logging
@macro
function myMacro(target: ClassDecl): ClassDecl {
// Logs during compilation
comptime.log(`Processing class: ${target.name}`)
comptime.log(`Properties: ${target.properties.length}`)
if (target.properties.length === 0) {
comptime.warn(`Class ${target.name} has no properties`)
}
return target
}Error Reporting
@macro
function requireFields(target: ClassDecl): ClassDecl {
const required = ["id", "name"]
const missing = required.filter(
f => !target.properties.some(p => p.name === f)
)
if (missing.length > 0) {
// Compile-time error with location
comptime.error(
`Missing required fields: ${missing.join(", ")}`,
target.location
)
}
return target
}Procedural Macros
For complex transformations, use procedural macros with full AST access.
@proc_macro
function router(input: TokenStream): TokenStream {
const routes = parseRoutes(input)
return generateRouter(routes)
}
// Usage in a separate file
router! {
GET /users -> getUsers
GET /users/:id -> getUser
POST /users -> createUser
DELETE /users/:id -> deleteUser
}Best Practices
Do
- Keep macros simple - Complex logic belongs in regular functions
- Provide good errors - Use
comptime.error()with locations - Document generated code - Use
msc expandexamples in docs - Test macro output - Write tests for expanded code
- Use hygiene - Avoid polluting user's namespace
Don't
- Don't hide behavior - Generated code should be predictable
- Don't overuse macros - Regular functions are often clearer
- Don't break types - Generated code must type-check
- Don't create side effects - Macros should be pure transformations
Next Steps
- Compile-Time Power - Introduction to macros
- Language Reference - Complete syntax reference
- Standard Library - Built-in functions and types