Skip to content

Commit

Permalink
Start adding something that looks like a typechecker
Browse files Browse the repository at this point in the history
  • Loading branch information
dusty-phillips committed Sep 10, 2024
1 parent 872fc05 commit 3904dba
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/glimpse/error.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ pub type GlimpseImportError {
CircularDependencyError(module_name: String)
MissingImportError(module_name: String)
}

pub type TypeCheckError {
InvalidReturnType(function_name: String, got: String, expected: String)
InvalidName(name: String)
}
206 changes: 206 additions & 0 deletions src/glimpse/typecheck.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import glimpse
import glimpse/error
import pprint

pub type Type {
NilType
IntType
FloatType
StringType
}

pub type Environment {
Environment(definitions: dict.Dict(String, Type))
}

pub type TypeOut {
TypeOut(environment: Environment, type_: Type)
}

type TypeCheckResult =
Result(TypeOut, error.TypeCheckError)

pub fn to_string(type_: Type) -> String {
case type_ {
NilType -> "Nil"
IntType -> "Int"
FloatType -> "Float"
StringType -> "String"
}
}

pub fn to_glance(type_: Type) -> glance.Type {
case type_ {
NilType -> glance.NamedType("Nil", option.None, [])
IntType -> glance.NamedType("Int", option.None, [])
FloatType -> glance.NamedType("Float", option.None, [])
StringType -> glance.NamedType("String", option.None, [])
}
}

pub fn module(
package: glimpse.Package,
module_name: String,
) -> Result(Nil, error.TypeCheckError) {
todo
}

/// Takes a glance function as input and returns the same function, but
/// with the inferred return type if the original function did not have
/// a return type. Returns an error if anything in the function doesn't
/// typecheck.
pub fn function(
environment: Environment,
function: glance.Function,
) -> Result(glance.Function, error.TypeCheckError) {
use environment <- result.try(list.fold_until(
function.parameters,
Ok(environment),
fold_function_parameter,
))

case block(environment, function.body) {
Error(err) -> Error(err)
Ok(block_out) ->
case function.return {
option.None -> {
Ok(
glance.Function(
..function,
return: option.Some(to_glance(block_out.type_)),
),
)
}
option.Some(expected_type) -> {
case type_(environment, expected_type) {
Error(err) -> Error(err)
Ok(expected) if expected != block_out.type_ ->
Error(error.InvalidReturnType(
function.name,
block_out.type_ |> to_string,
expected |> to_string,
))
Ok(_) -> Ok(function)
}
}
}
}
}

fn fold_function_parameter(
state: Result(Environment, error.TypeCheckError),
param: glance.FunctionParameter,
) -> list.ContinueOrStop(Result(Environment, error.TypeCheckError)) {
case state {
Error(_err) -> list.Stop(state)
Ok(environment) ->
{
case param {
glance.FunctionParameter(name: glance.Discarded(_), ..) ->
Ok(environment)
glance.FunctionParameter(type_: option.None, ..) ->
todo as "Not inferring untyped parameters yet"
glance.FunctionParameter(
name: glance.Named(name),
type_: option.Some(glance_type),
..,
) -> {
use check_type <- result.try(type_(environment, glance_type))
Ok(add_def(environment, name, check_type))
}
}
}
|> list.Continue
}
}

pub fn block(
environment: Environment,
statements: List(glance.Statement),
) -> TypeCheckResult {
list.fold_until(
statements,
Ok(TypeOut(environment, NilType)),
fn(state, stmnt) {
case state {
Error(_) -> list.Stop(state)
Ok(type_out) -> list.Continue(statement(type_out.environment, stmnt))
}
},
)
}

pub fn statement(
environment: Environment,
statement: glance.Statement,
) -> TypeCheckResult {
case statement {
glance.Expression(expr) -> expression(environment, expr)
_ -> todo
}
}

pub fn expression(
environment: Environment,
expression: glance.Expression,
) -> TypeCheckResult {
case expression {
// TODO: Not 100% sure this will ever need to update the environment,
// so we may be able to remove it from the return
glance.Int(_) -> Ok(TypeOut(environment, IntType))
glance.Float(_) -> Ok(TypeOut(environment, FloatType))
glance.String(_) -> Ok(TypeOut(environment, StringType))
glance.Variable("Nil") -> Ok(TypeOut(environment, NilType))
glance.Variable(name) -> lookup_type_out(environment, name)
_ -> {
pprint.debug(expression)
todo
}
}
}

pub fn type_(
environment: Environment,
glance_type: glance.Type,
) -> Result(Type, error.TypeCheckError) {
case glance_type {
glance.NamedType("Int", option.None, []) -> Ok(IntType)
glance.NamedType("Float", option.None, []) -> Ok(FloatType)
glance.NamedType("Nil", option.None, []) -> Ok(NilType)
glance.NamedType("String", option.None, []) -> Ok(StringType)
glance.VariableType(name) -> lookup_type(environment, name)
_ -> {
pprint.debug(glance_type)
todo
}
}
}

fn add_def(environment: Environment, name: String, type_: Type) -> Environment {
Environment(
// ..environment,
definitions: dict.insert(environment.definitions, name, type_),
)
}

fn lookup_type(
environment: Environment,
name: String,
) -> Result(Type, error.TypeCheckError) {
dict.get(environment.definitions, name)
|> result.replace_error(error.InvalidName(name))
}

fn lookup_type_out(
environment: Environment,
name: String,
) -> Result(TypeOut, error.TypeCheckError) {
dict.get(environment.definitions, name)
|> result.replace_error(error.InvalidName(name))
|> result.map(TypeOut(environment, _))
}
11 changes: 11 additions & 0 deletions test/typecheck/function_params_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import glance
import gleam/option
import gleeunit/should
import typecheck/helpers

pub fn int_param_test() {
let function_out = helpers.ok_typecheck("fn foo(a: Int) -> Int { a }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Int", option.None, [])))
}
34 changes: 34 additions & 0 deletions test/typecheck/helpers.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import glance
import gleam/dict
import gleam/list
import gleeunit/should
import glimpse/error
import glimpse/typecheck

// Helpers
pub fn glance_function(definition: String) -> glance.Function {
let module =
glance.module(definition)
|> should.be_ok()

module.functions |> list.length |> should.equal(1)

let definition = list.first(module.functions) |> should.be_ok()
definition.definition
}

pub fn blank_env() -> typecheck.Environment {
typecheck.Environment(dict.new())
}

pub fn ok_typecheck(definition: String) -> glance.Function {
let function = glance_function(definition)
typecheck.function(blank_env(), function)
|> should.be_ok
}

pub fn error_typecheck(definition: String) -> error.TypeCheckError {
let function = glance_function(definition)
typecheck.function(blank_env(), function)
|> should.be_error
}
69 changes: 69 additions & 0 deletions test/typecheck/primitive_types_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import glance
import gleam/option
import gleeunit/should
import glimpse/error
import typecheck/helpers

pub fn return_nil_test() {
let function_out = helpers.ok_typecheck("fn foo() -> Nil { Nil }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Nil", option.None, [])))
}

pub fn infer_nil_test() {
let function_out = helpers.ok_typecheck("fn foo() { }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Nil", option.None, [])))
}

pub fn not_nil_error_test() {
helpers.error_typecheck("fn foo() -> Nil { 5 }")
|> should.equal(error.InvalidReturnType("foo", "Int", "Nil"))
}

pub fn return_int_test() {
let function_out = helpers.ok_typecheck("fn foo() -> Int { 5 }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Int", option.None, [])))
}

pub fn infer_int_test() {
let function_out = helpers.ok_typecheck("fn foo() { 5 }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Int", option.None, [])))
}

pub fn not_int_error_test() {
helpers.error_typecheck("fn foo() -> Int { Nil }")
|> should.equal(error.InvalidReturnType("foo", "Nil", "Int"))
}

pub fn return_float_test() {
let function_out = helpers.ok_typecheck("fn foo() -> Float { 5.0 }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Float", option.None, [])))
}

pub fn infer_float_test() {
let function_out = helpers.ok_typecheck("fn foo() { 5.0 }")

function_out.return
|> should.equal(option.Some(glance.NamedType("Float", option.None, [])))
}

pub fn not_float_error_test() {
helpers.error_typecheck("fn foo() -> Float { 5 }")
|> should.equal(error.InvalidReturnType("foo", "Int", "Float"))
}

pub fn return_string_test() {
let function_out = helpers.ok_typecheck("fn foo() -> String { \"hello\" }")

function_out.return
|> should.equal(option.Some(glance.NamedType("String", option.None, [])))
}

0 comments on commit 3904dba

Please sign in to comment.