Skip to main content

src/webql/compiler.gleam

import gleam/list
import gleam/result
import webql/compiler/context
import webql/compiler/diagnostic
import webql/compiler/environment
import webql/compiler/lexer
import webql/compiler/lowerer
import webql/compiler/parser
import webql/compiler/resolver
import webql/compiler/typechecker
import webql/graph
import webql/introspection

pub opaque type Compiler {
  Compiler(environment: environment.Environment)
}

/// Creates a compiler instance with resolver context.
pub fn new(schema: introspection.Schema) -> Compiler {
  let introspection.Schema(operations:, ports:) = schema

  let environment =
    list.fold(
      operations,
      environment.add_ports(environment.new(), ports),
      load_operation,
    )

  Compiler(environment:)
}

/// Compiles a text source into a finalized document.
pub fn compile(
  compiler: Compiler,
  source: String,
) -> Result(graph.Graph, diagnostic.Diagnostic) {
  let context = context.new()

  let lexer = lexer.new(source)
  use tokens <- result.try(compile_lex(lexer))

  let parser = parser.new(source, tokens)
  use document <- result.try(compile_parse(parser))

  let resolver = resolver.new(document)
  use #(document, context) <- result.try(compile_resolve(
    compiler,
    context,
    resolver,
  ))

  let typechecker = typechecker.new(document)
  use document <- result.try(compile_typecheck(typechecker, context))

  let lowerer = lowerer.new(document)
  Ok(lowerer.lower(lowerer))
}

// PRIVATE FUNCTIONS
// =================
fn load_operation(
  environment: environment.Environment,
  operation: introspection.Operation,
) -> environment.Environment {
  let introspection.Operation(name:, inputs:, outputs:) = operation

  environment
  |> environment.add_operation(name)
  |> load_parameters(name, inputs)
  |> load_returns(name, outputs)
}

fn load_parameters(
  environment: environment.Environment,
  operation: String,
  inputs: List(introspection.Input),
) -> environment.Environment {
  use environment, input <- list.fold(inputs, environment)
  let introspection.Input(name:, port:) = input

  let environment = environment.add_port(environment, port)
  let operation = environment.get_operation(environment, operation)
  let port = environment.get_port(environment, port)

  case operation, port {
    Ok(operation), Ok(port) ->
      environment.add_input(environment, operation, #(name, port))

    _operation, _port -> environment
  }
}

fn load_returns(
  environment: environment.Environment,
  operation: String,
  outputs: List(introspection.Output),
) -> environment.Environment {
  use environment, output <- list.fold(outputs, environment)
  let introspection.Output(name:, port:) = output

  let environment = environment.add_port(environment, port)
  let operation = environment.get_operation(environment, operation)
  let port = environment.get_port(environment, port)

  case operation, port {
    Ok(operation), Ok(port) ->
      environment.add_output(environment, operation, #(name, port))

    _operation, _port -> environment
  }
}

fn compile_lex(lexer: lexer.Lexer) {
  case lexer.lex(lexer) {
    Ok(tokens) -> Ok(tokens)

    Error(error) ->
      Error(diagnostic.Diagnostic(
        kind: diagnostic.LexerDiagnostic(error.kind),
        span: error.span,
      ))
  }
}

fn compile_parse(parser: parser.Parser) {
  case parser.parse(parser) {
    Ok(document) -> Ok(document)

    Error(error) ->
      Error(diagnostic.Diagnostic(
        kind: diagnostic.ParserDiagnostic(error.kind),
        span: error.span,
      ))
  }
}

fn compile_resolve(
  compiler: Compiler,
  context: context.Context,
  resolver: resolver.Resolver,
) {
  case resolver.resolve(resolver, compiler.environment, context) {
    Ok(document) -> Ok(document)

    Error(error) ->
      Error(diagnostic.Diagnostic(
        kind: diagnostic.ResolverDiagnostic(error.kind),
        span: error.span,
      ))
  }
}

fn compile_typecheck(
  typechecker: typechecker.Typechecker,
  context: context.Context,
) {
  case typechecker.resolve(typechecker, context) {
    Ok(document) -> Ok(document)

    Error(error) ->
      Error(diagnostic.Diagnostic(
        kind: diagnostic.TypecheckerDiagnostic(error.kind),
        span: error.span,
      ))
  }
}