sapphire-lang 0.8.0

Gradually typed scripting language where every value is an object and types are optional, checked at runtime
Documentation
use sapphire::compiler::compile;
use sapphire::lexer::Lexer;
use sapphire::parser::Parser;
use sapphire::typechecker::TypeChecker;
use sapphire::vm::{Vm, VmValue};
use std::path::PathBuf;

fn parse(src: &str) -> Vec<sapphire::ast::Expr> {
    let tokens = Lexer::new(src).scan_tokens();
    Parser::new(tokens).parse().expect("parse error")
}

fn typecheck_ok(src: &str) {
    let stmts = parse(src);
    let errors = TypeChecker::check(&stmts);
    assert!(errors.is_empty(), "unexpected type errors: {:?}", errors);
}

fn typecheck_err_msg(src: &str) -> String {
    let stmts = parse(src);
    let errors = TypeChecker::check(&stmts);
    assert!(!errors.is_empty(), "expected type errors for:\n{src}");
    errors[0].message.clone()
}

macro_rules! assert_typecheck_error {
    ($src:expr, $($substring:expr),+ $(,)?) => {{
        let msg = typecheck_err_msg($src);
        $(
        assert!(
            msg.contains($substring),
            "expected first type error to contain:\n{}\n\nmessage:\n{}",
            $substring,
            msg
        );
        )+
    }};
}

fn eval(src: &str) -> VmValue {
    let stmts = parse(src);
    let func = compile(&stmts).expect("compile error");
    Vm::new(func, PathBuf::new())
        .run()
        .expect("vm error")
        .expect("empty stack")
}

#[test]
fn class_structurally_satisfies_interface_without_declaration() {
    typecheck_ok(
        r#"
interface Drawable {
  def draw -> String
}

class Circle {
  def draw -> String { "circle" }
}

def render(item: Drawable) -> String {
  item.draw()
}

render(Circle.new())
"#,
    );
}

#[test]
fn interface_annotation_is_static_only_at_runtime() {
    let value = eval(
        r#"
interface Drawable {
  def draw -> String
}

class Circle {
  def draw -> String { "circle" }
}

def render(item: Drawable) -> String {
  item.draw()
}

render(Circle.new())
"#,
    );

    assert_eq!(value, VmValue::Str("circle".into()));
}

#[test]
fn missing_interface_method_is_type_error() {
    assert_typecheck_error!(
        r#"
interface Drawable {
  def draw -> String
}

class Circle {
}

def render(item: Drawable) -> String {
  item.draw()
}

render(Circle.new())
"#,
        "Circle",
        "Drawable",
        "draw",
    );
}

#[test]
fn interface_method_return_must_match() {
    assert_typecheck_error!(
        r#"
interface Drawable {
  def draw -> String
}

class Circle {
  def draw -> Int { 1 }
}

def render(item: Drawable) -> String {
  item.draw()
}

render(Circle.new())
"#,
        "Circle",
        "Drawable",
        "draw",
        "expected String",
        "got Int",
    );
}

#[test]
fn interface_typed_value_only_exposes_interface_methods() {
    assert_typecheck_error!(
        r#"
interface Drawable {
  def draw -> String
}

class Circle {
  def draw -> String { "circle" }
  def radius -> Int { 5 }
}

def render(item: Drawable) -> Int {
  item.radius()
}

render(Circle.new())
"#,
        "method 'radius' is not defined by interface Drawable",
    );
}

#[test]
fn generic_interface_substitutes_type_arguments() {
    typecheck_ok(
        r#"
interface Sink[T] {
  def push(value: T) -> Nil
}

class StringSink {
  def push(value: String) -> Nil { nil }
}

def write(sink: Sink[String]) -> Nil {
  sink.push("hello")
}

write(StringSink.new())
"#,
    );
}

#[test]
fn generic_interface_rejects_wrong_type_argument() {
    assert_typecheck_error!(
        r#"
interface Sink[T] {
  def push(value: T) -> Nil
}

class IntSink {
  def push(value: Int) -> Nil { nil }
}

def write(sink: Sink[String]) -> Nil {
  sink.push("hello")
}

write(IntSink.new())
"#,
        "IntSink",
        "Sink[String]",
        "push",
        "expected String",
        "got Int",
    );
}

#[test]
fn included_module_methods_count_for_structural_interface() {
    typecheck_ok(
        r#"
interface Greeter {
  def greet -> String
}

module Greeting {
  def greet -> String { "hi" }
}

class Person {
  include(Greeting)
}

def greet(person: Greeter) -> String {
  person.greet()
}

greet(Person.new())
"#,
    );
}