Pre-AlphaMetaScript is in early design phase. The compiler is not yet available.Join Discord for updates
MetaScript

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 expand to 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 derive

Built-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

TraitGeneratesMethod Signature
EqEquality comparisonequals(other: Self): boolean
HashHash code computationhashCode(): number
CloneDeep copyclone(): Self
DebugDebug stringtoString(): string
SerializeJSON serializationtoJSON(): object
DeserializeJSON parsingstatic fromJSON(json: object): Self
DefaultDefault instancestatic default(): Self
OrdOrdering comparisoncompareTo(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

FeatureExample
File operationsfs.readFileSync(), fs.readdirSync()
Environmentenv.get("NODE_ENV")
JSON/YAML parsingJSON.parse(), YAML.parse()
HTTP requestshttp.get() (cached)
Code generationReturn AST nodes
Math operationsAll 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

OptionTypeDescription
rename_all"camelCase" | "snake_case" | "PascalCase" | "SCREAMING_SNAKE"Field naming convention
skip_nullbooleanOmit null/undefined fields
skip_defaultbooleanOmit fields with default values
flattenstring[]Flatten nested object fields
deny_unknownbooleanError 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

OptionTypeDescription
namestringCustom test name
timeoutnumberTimeout in milliseconds
skipbooleanSkip this test
onlybooleanRun only this test
retrynumberRetry 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 TypeDescriptionCommon Use
ClassDeclClass declarationAdd methods, properties
FunctionDeclFunction declarationWrap, modify parameters
PropertyDeclProperty declarationAdd decorators, change type
MethodDeclMethod declarationAdd logging, validation
ExpressionAny expressionTransform values
StatementAny statementAdd 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

OperatorDescription
${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.ms

Compile-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 expand examples 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