From b6346ba46655f0fb0b942680afde2d57c6759b25 Mon Sep 17 00:00:00 2001 From: Aron Zwaan Date: Fri, 24 May 2024 17:14:21 +0200 Subject: [PATCH 1/7] Example test infrastructure --- scopegraphs/examples/records.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scopegraphs/examples/records.rs b/scopegraphs/examples/records.rs index 226bf38..cdb51ce 100644 --- a/scopegraphs/examples/records.rs +++ b/scopegraphs/examples/records.rs @@ -228,7 +228,7 @@ impl UnionFind { mod ast { use std::collections::HashMap; - #[derive(Debug, Clone)] + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Type { StructRef(String), Int, @@ -817,3 +817,19 @@ in a.b.a.x; Ok(()) } + +#[cfg(test)] +mod tests { + use crate::{ast, parse::parse, typecheck}; + + fn test_example(program: &str, expected_main_type: ast::Type) { + let ast = parse(program).expect("parse failure"); + let ty = typecheck(&ast).expect("type not instantiated"); + assert_eq!(ty, expected_main_type) + } + + #[test] + fn test_integer() { + test_example(" main = 42; ", ast::Type::Int) + } +} From 962aa1b4686e0fb1144027a90673f1da89590855 Mon Sep 17 00:00:00 2001 From: Aron Zwaan Date: Fri, 24 May 2024 15:21:04 +0200 Subject: [PATCH 2/7] Test examples in CI [build should fail on purpose] --- .github/workflows/rust.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 23e1acd..dbc76ca 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -76,6 +76,12 @@ jobs: command: test args: --all-features + - name: Run cargo examples tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --examples + test-stable: name: Test Stable runs-on: ubuntu-latest @@ -92,3 +98,9 @@ jobs: with: command: test args: --all-features + + - name: Run cargo examples tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --examples From bbc444a05f3345a292a35e7c22ab4aa531085f6a Mon Sep 17 00:00:00 2001 From: Aron Zwaan Date: Fri, 24 May 2024 15:23:06 +0200 Subject: [PATCH 3/7] Also add tests for nightly, remove failing test --- .github/workflows/rust.yml | 6 ++++++ scopegraphs/examples/records.rs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dbc76ca..3743a20 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -59,6 +59,12 @@ jobs: command: test args: --all-features + - name: Run cargo examples tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --examples + test-beta: name: Test Beta runs-on: ubuntu-latest diff --git a/scopegraphs/examples/records.rs b/scopegraphs/examples/records.rs index cdb51ce..45010f9 100644 --- a/scopegraphs/examples/records.rs +++ b/scopegraphs/examples/records.rs @@ -817,6 +817,7 @@ in a.b.a.x; Ok(()) } +<<<<<<< HEAD #[cfg(test)] mod tests { @@ -833,3 +834,5 @@ mod tests { test_example(" main = 42; ", ast::Type::Int) } } +======= +>>>>>>> fe89878 (Also add tests for nightly, remove failing test) From 4fbd49172fc0e531a07799acd3eb87058f68a348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Mon, 27 May 2024 16:54:41 +0200 Subject: [PATCH 4/7] add nix setups --- .envrc | 1 + .gitignore | 3 ++- flake.nix | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .envrc create mode 100644 flake.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..44610e5 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake; diff --git a/.gitignore b/.gitignore index 083e81b..5ac8096 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ Cargo.lock .vscode/ .idea/ -*.dot \ No newline at end of file +*.dot +.direnv \ No newline at end of file diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f4da116 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + }; + + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + in { + devShells.default = pkgs.mkShell rec { + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + targets = [ "wasm32-unknown-unknown" ]; + extensions = [ "rust-src" "rust-analyzer" ]; + }; + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + + buildInputs = with pkgs; [ trunk rustToolchain cargo-watch ]; + }; + }); +} From d3b357c434408fb398f695e714f8561c364f1b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Mon, 27 May 2024 17:32:02 +0200 Subject: [PATCH 5/7] add flake.lock --- flake.lock | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 flake.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6a5757a --- /dev/null +++ b/flake.lock @@ -0,0 +1,85 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1716509168, + "narHash": "sha256-4zSIhSRRIoEBwjbPm3YiGtbd8HDWzFxJjw5DYSDy1n8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bfb7a882678e518398ce9a31a881538679f6f092", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1716776264, + "narHash": "sha256-fYzMk5o//g5Wt1g0FyOC8/XVllbGdVdzdylXxcanakU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8ef3f6a8f5af867ab5f75fc86fbd934a6351820b", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} From c42ad8e657d18f4443eec739425fd0a0347a33c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Mon, 27 May 2024 18:40:12 +0200 Subject: [PATCH 6/7] add more tests --- scopegraphs/examples/records.rs | 838 --------------------- scopegraphs/examples/records/ast.rs | 43 ++ scopegraphs/examples/records/main.rs | 424 +++++++++++ scopegraphs/examples/records/parse.rs | 191 +++++ scopegraphs/examples/records/queries.rs | 70 ++ scopegraphs/examples/records/tests.rs | 44 ++ scopegraphs/examples/records/union_find.rs | 153 ++++ 7 files changed, 925 insertions(+), 838 deletions(-) delete mode 100644 scopegraphs/examples/records.rs create mode 100644 scopegraphs/examples/records/ast.rs create mode 100644 scopegraphs/examples/records/main.rs create mode 100644 scopegraphs/examples/records/parse.rs create mode 100644 scopegraphs/examples/records/queries.rs create mode 100644 scopegraphs/examples/records/tests.rs create mode 100644 scopegraphs/examples/records/union_find.rs diff --git a/scopegraphs/examples/records.rs b/scopegraphs/examples/records.rs deleted file mode 100644 index 45010f9..0000000 --- a/scopegraphs/examples/records.rs +++ /dev/null @@ -1,838 +0,0 @@ -use crate::ast::{Expr, Program, RecordDef, Type}; -use crate::resolve::{resolve_lexical_ref, resolve_member_ref, resolve_record_ref}; -use async_recursion::async_recursion; -use futures::future::{join, join_all}; -use scopegraphs::completeness::FutureCompleteness; -use scopegraphs::RenderScopeData; -use scopegraphs::{Scope, ScopeGraph, Storage}; -use scopegraphs_macros::Label; -use smol::channel::{bounded, Sender}; -use smol::LocalExecutor; -use std::cell::RefCell; -use std::error::Error; -use std::fmt::{Debug, Formatter}; -use std::fs::File; -use std::future::Future; -use std::rc::Rc; - -#[derive(Debug, Label, Copy, Clone, Hash, PartialEq, Eq)] -enum SgLabel { - TypeDefinition, - Definition, - Lexical, -} - -#[derive(Debug, Default, Hash, Eq, PartialEq, Clone)] -enum SgData { - VarDecl { - name: String, - ty: PartialType, - }, - TypeDecl { - name: String, - scope: Scope, - }, - - #[default] - Nothing, -} - -impl RenderScopeData for SgData { - fn render(&self) -> Option { - match self { - SgData::VarDecl { name, ty } => Some(format!("var {name}: {ty:?}")), - SgData::TypeDecl { name, scope } => Some(format!("record {name} -> {scope:?}")), - SgData::Nothing => None, - } - } -} - -impl SgData { - pub fn expect_var_decl(&self) -> &PartialType { - match self { - SgData::VarDecl { ty, .. } => ty, - _ => panic!("expected var decl, got {:?}", &self), - } - } - - pub fn expect_type_decl(&self) -> &Scope { - match self { - SgData::TypeDecl { scope: ty, .. } => ty, - _ => panic!("expected type decl, got {:?}", &self), - } - } -} - -/// A type variable, a placeholder for a type -#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Copy)] -pub struct TypeVar(usize); - -/// A partial type can either still be a type variable, or a concrete type -#[derive(Clone, Debug, Hash, Eq, PartialEq)] -enum PartialType { - /// A variable - Variable(TypeVar), - /// A record named `name` with a scope. - /// The scope contains the field of the record. - /// See "scopes as types" - Record { name: String, scope: Scope }, - /// A number type - Int, -} - -#[derive(Default)] -pub struct UnionFind { - /// Records the parent of each type variable. - /// Kind of assumes type variables are assigned linearly. - /// - /// For example the "parent" of type variable 0 is stored at index 0 - parent: Vec, - /// Keep track of type variables we've given out - vars: usize, - /// A vec of signals for each type variable. - /// - /// For example, whenever type variable 0 is unified with anything, - /// we go through the list at index 0 and notify each. - callbacks: Vec>>, -} - -impl Debug for UnionFind { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{{")?; - for (idx, p) in self.parent.iter().enumerate() { - write!(f, "{idx} -> {p:?}")?; - if (idx + 1) < self.parent.len() { - write!(f, ", ")?; - } - } - write!(f, "}}") - } -} - -impl UnionFind { - /// Create a new type variable - /// (which happens to be one bigger than the previous fresh type variable) - fn fresh(&mut self) -> TypeVar { - let old = self.vars; - self.vars += 1; - - TypeVar(old) - } - - /// Unify two partial types, asserting they are equal to each other. - /// - /// If one of left or right is a concrete type, and the other is a type variable, - /// we've essentially resolved what type the type variable is now, and we update the - /// data structure to represent that. The next [`find`](Self::find) of this type variable - /// will return the concrete type after this unification. - /// - /// Sometimes, two type variables are unified. In that case, one of the two is chosen by - /// a fair (trust me) dice roll and is made the representative of both input type variables. - /// Whenever one of the two is now unified with a concrete type, both input type variables - /// become equal to that concrete type. - fn unify(&mut self, left: PartialType, right: PartialType) { - let left = self.find_partial_type(left); - let right = self.find_partial_type(right); - - match (left, right) { - (PartialType::Variable(left), right) | (right, PartialType::Variable(left)) => { - // FIXME: use rank heuristic in case right is a variable? - *self.get(left) = right.clone(); - if self.callbacks.len() > left.0 { - for fut in self.callbacks[left.0].drain(..) { - let _ = fut.send_blocking(right.clone()); - } - } - } - (left, right) if left != right => { - panic!("type error: cannot unify {left:?} and {right:?}"); - } - _ => {} - } - } - - /// Find the representative for a given type variable. - /// In the best case, this is a concrete type this type variable is equal to. - /// That's nice, because now we know what that type variable was supposed to be. - /// - /// However, it's possible we find another type variable instead (wrapped in a [`PartialType`]). - /// Now we know that this new type variable has the same type of the given type variable, - /// we just don't know yet which type that is. More unifications are needed. - fn find(&mut self, ty: TypeVar) -> PartialType { - let res = self.get(ty); - if let PartialType::Variable(v) = *res { - if v == ty { - return PartialType::Variable(ty); - } - - // do path compression - let root = self.find(v); - *self.get(v) = root.clone(); - root - } else { - res.clone() - } - } - - /// [find](Self::find), but for a parial type - fn find_partial_type(&mut self, ty: PartialType) -> PartialType { - if let PartialType::Variable(v) = ty { - self.find(v) - } else { - ty - } - } - - /// Get a mutable reference to parent of a given type variable. - /// Used in the implementation of [`find`](Self::find) and [`union`](Self::union) - fn get(&mut self, tv: TypeVar) -> &mut PartialType { - let parent = &mut self.parent; - for i in parent.len()..=tv.0 { - parent.push(PartialType::Variable(TypeVar(i))); - } - - &mut parent[tv.0] - } - - #[allow(unused)] - fn type_of(&mut self, var: TypeVar) -> Option { - match self.find(var) { - PartialType::Variable(_) => None, - PartialType::Record { name, .. } => Some(Type::StructRef(name)), - PartialType::Int => Some(Type::Int), - } - } - - fn type_of_partial_type(&mut self, var: PartialType) -> Option { - match self.find_partial_type(var) { - PartialType::Variable(_) => None, - PartialType::Record { name, .. } => Some(Type::StructRef(name)), - PartialType::Int => Some(Type::Int), - } - } - - /// Wait for when tv is unified with something. - fn wait_for_unification(&mut self, tv: TypeVar) -> impl Future { - let callbacks = &mut self.callbacks; - for _ in callbacks.len()..=tv.0 { - callbacks.push(vec![]); - } - - let (tx, rx) = bounded(1); - callbacks[tv.0].push(tx); - - async move { rx.recv().await.expect("sender dropped") } - } -} - -mod ast { - use std::collections::HashMap; - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum Type { - StructRef(String), - Int, - } - - #[derive(Debug)] - pub struct RecordDef { - pub name: String, - pub fields: HashMap, - } - - #[derive(Debug, Clone)] - pub enum Expr { - StructInit { - name: String, - fields: HashMap, - }, - #[allow(unused)] - Add(Box, Box), - Number(u64), - Ident(String), - FieldAccess(Box, String), - #[allow(unused)] - Let { - name: String, - value: Box, - in_expr: Box, - }, - LetRec { - values: HashMap, - in_expr: Box, - }, - } - - #[derive(Debug)] - pub struct Program { - /// Items can occur in any order. Like in Rust! - pub record_types: Vec, - pub main: Expr, - } -} - -type RecordScopegraph<'sg> = ScopeGraph<'sg, SgLabel, SgData, FutureCompleteness>; - -struct TypeChecker<'sg, 'ex> { - sg: RecordScopegraph<'sg>, - uf: RefCell, - ex: LocalExecutor<'ex>, -} - -impl<'sg, 'ex> TypeChecker<'sg, 'ex> -where - 'sg: 'ex, -{ - fn spawn(self: &Rc, f: impl FnOnce(Rc) -> F) - where - F: Future + 'ex, - T: 'ex, - { - self.ex.spawn(f(self.clone())).detach() - } - - #[async_recursion(?Send)] - async fn typecheck_expr<'a>(self: Rc, ast: &'ex Expr, scope: Scope) -> PartialType { - match ast { - Expr::StructInit { name, fields } => { - let record_scope = resolve_record_ref(&self.sg, scope, name).await; - - // defer typechecking of all the fields.. - for (field_name, field_initializer) in fields { - self.spawn(|this| async move { - let (decl_type, init_type) = join( - resolve_member_ref(&this.sg, record_scope, field_name), - this.clone().typecheck_expr(field_initializer, scope), - ) - .await; - - this.uf.borrow_mut().unify(decl_type, init_type); - }); - } - - // FIXME: field init exhaustiveness check omitted - - // .. but eagerly return the record type - PartialType::Record { - name: name.clone(), - scope: record_scope, - } - } - Expr::Add(l, r) => { - // type check left-hand-side asynchronously - self.spawn(|this| async move { - let l_ty = this.clone().typecheck_expr(l, scope).await; - this.uf.borrow_mut().unify(l_ty, PartialType::Int); - }); - // and type-check the right-hand-side asynchronously - self.spawn(|this| async move { - let r_ty = this.clone().typecheck_expr(r, scope).await; - this.uf.borrow_mut().unify(r_ty, PartialType::Int); - }); - - // ... but immediately return the current type - PartialType::Int - } - Expr::Number(_) => PartialType::Int, - Expr::Ident(var_name) => resolve_lexical_ref(&self.sg, scope, var_name).await, - Expr::FieldAccess(inner, field) => { - let res = self.clone().typecheck_expr(inner, scope).await; - let inner_expr_type = self.uf.borrow_mut().find_partial_type(res); - self.type_check_field_access(inner_expr_type, field).await - } - Expr::Let { - name, - value, - in_expr, - } => { - let new_scope = self - .sg - .add_scope_default_with([SgLabel::Lexical, SgLabel::Definition]); - self.sg - .add_edge(new_scope, SgLabel::Lexical, scope) - .expect("already closed"); - self.sg.close(new_scope, &SgLabel::Lexical); - - let ty_var = PartialType::Variable(self.uf.borrow_mut().fresh()); - self.sg - .add_decl( - new_scope, - SgLabel::Definition, - SgData::VarDecl { - name: name.clone(), - ty: ty_var.clone(), - }, - ) - .expect("already closed"); - - self.sg.close(new_scope, &SgLabel::Definition); - - self.spawn(|this| async move { - let ty = this.clone().typecheck_expr(value, scope).await; - this.uf.borrow_mut().unify(ty_var, ty); - }); - - // compute type of the result expression - self.clone().typecheck_expr(in_expr, new_scope).await - } - Expr::LetRec { values, in_expr } => { - let new_scope = self - .sg - .add_scope_default_with([SgLabel::Lexical, SgLabel::Definition]); - self.sg - .add_edge(new_scope, SgLabel::Lexical, scope) - .expect("already closed"); - self.sg.close(new_scope, &SgLabel::Lexical); - - for (name, initializer_expr) in values { - let ty = PartialType::Variable(self.uf.borrow_mut().fresh()); - self.sg - .add_decl( - new_scope, - SgLabel::Definition, - SgData::VarDecl { - name: name.clone(), - ty: ty.clone(), - }, - ) - .expect("already closed"); - - self.spawn(|this| async move { - let init_ty = this - .clone() - .typecheck_expr(initializer_expr, new_scope) - .await; - this.uf.borrow_mut().unify(ty, init_ty) - }); - } - self.sg.close(new_scope, &SgLabel::Definition); - - // compute type of the result expression - self.typecheck_expr(in_expr, new_scope).await - } - } - } - - async fn type_check_field_access( - self: Rc, - mut inner_expr_type: PartialType, - field: &str, - ) -> PartialType { - loop { - match inner_expr_type { - PartialType::Variable(tv) => { - let fut = self.uf.borrow_mut().wait_for_unification(tv); - inner_expr_type = fut.await; - } - PartialType::Record { scope, .. } => { - break resolve_member_ref(&self.sg, scope, field).await - } - PartialType::Int => panic!("number has no field {field}"), - } - } - } - - fn init_record_def(&self, record_def: &RecordDef, scope: Scope) -> Scope { - let field_scope = self.sg.add_scope_default_with([SgLabel::Definition]); - self.sg - .add_decl( - scope, - SgLabel::TypeDefinition, - SgData::TypeDecl { - name: record_def.name.clone(), - scope: field_scope, - }, - ) - .expect("already closed"); - self.sg.close(scope, &SgLabel::Definition); - - field_scope - } - - async fn typecheck_record_def( - self: Rc, - record_def: &RecordDef, - scope: Scope, - field_scope: Scope, - ) { - let fld_decl_futures = record_def.fields.iter().map(|(fld_name, fld_ty)| { - let this = self.clone(); - async move { - let ty = match fld_ty { - Type::StructRef(n) => { - let record_scope = resolve_record_ref(&this.sg, scope, n).await; - PartialType::Record { - name: n.clone(), - scope: record_scope, - } - } - Type::Int => PartialType::Int, - }; - - this.sg - .add_decl( - field_scope, - SgLabel::Definition, - SgData::VarDecl { - name: fld_name.clone(), - ty, - }, - ) - .expect("unexpected close"); - } - }); - - join_all(fld_decl_futures).await; - self.sg.close(field_scope, &SgLabel::Definition); - } -} - -mod resolve { - use crate::{PartialType, RecordScopegraph, SgData, SgLabel}; - use scopegraphs::resolve::Resolve; - use scopegraphs::{query_regex, Scope}; - use scopegraphs_macros::label_order; - - pub async fn resolve_record_ref( - sg: &RecordScopegraph<'_>, - scope: Scope, - ref_name: &str, - ) -> Scope { - let env = sg - .query() - .with_path_wellformedness(query_regex!(SgLabel: Lexical* TypeDefinition)) - .with_data_wellformedness(|record_data: &SgData| match record_data { - SgData::TypeDecl { - name: decl_name, .. - } => decl_name == ref_name, - _ => false, - }) - .with_label_order(label_order!(SgLabel: Definition < Lexical)) - .resolve(scope) - .await; - - *env.get_only_item() - .expect("record name did not resolve properly") - .data() - .expect_type_decl() - } - - pub async fn resolve_lexical_ref( - sg: &RecordScopegraph<'_>, - scope: Scope, - var_name: &str, - ) -> PartialType { - let env = sg - .query() - .with_path_wellformedness(query_regex!(SgLabel: Lexical* Definition)) - .with_label_order(label_order!(SgLabel: Lexical < Definition)) - .with_data_wellformedness(|record_data: &SgData| -> bool { - matches!(record_data, SgData::VarDecl { name, .. } if name == var_name) - }) - .resolve(scope) - .await; - - env.get_only_item() - .expect("variable did not resolve uniquely") - .data() - .expect_var_decl() - .clone() - } - - pub async fn resolve_member_ref( - sg: &RecordScopegraph<'_>, - record_scope: Scope, - ref_name: &str, - ) -> PartialType { - let env = sg - .query() - .with_path_wellformedness(query_regex!(SgLabel: Definition)) - .with_data_wellformedness(|record_data: &SgData| match record_data { - SgData::VarDecl { - name: decl_name, .. - } => decl_name == ref_name, - _ => false, - }) - .resolve(record_scope) - .await; - - env.get_only_item() - .expect("field name did not resolve properly") - .data() - .expect_var_decl() - .clone() - } -} - -fn typecheck(ast: &Program) -> Option { - let storage = Storage::new(); - let sg = RecordScopegraph::new(&storage, FutureCompleteness::default()); - let uf = RefCell::new(UnionFind::default()); - let local = LocalExecutor::new(); - - let tc = Rc::new(TypeChecker { sg, uf, ex: local }); - - let global_scope = tc.sg.add_scope_default_with([SgLabel::TypeDefinition]); - - // typecheck all the type definitions somewhere in the future - for item in &ast.record_types { - // synchronously init record decl - let field_scope = tc.init_record_def(item, global_scope); - tc.spawn(|this| this.typecheck_record_def(item, global_scope, field_scope)); - } - - // We can close for type definitions since the scopes for this are synchronously - // made even before the future is returned and spawned. so, at this point, - // no new type definitions are made. - tc.sg.close(global_scope, &SgLabel::TypeDefinition); - - // typecheck the main expression - let res = tc - .ex - .spawn(tc.clone().typecheck_expr(&ast.main, global_scope)); - - // extract result from task - let main_ty = smol::block_on(async { - while !tc.ex.is_empty() { - tc.ex.tick().await; - } - - res.await - }); - - tc.sg - .render(&mut File::create("sg.dot").unwrap(), "sg") - .unwrap(); - println!("{:?}", tc.uf.borrow()); - - let resolved_main_ty = tc.uf.borrow_mut().type_of_partial_type(main_ty); - resolved_main_ty -} - -mod parse { - use std::collections::HashMap; - use winnow::ascii::multispace0; - use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated, terminated}; - use winnow::error::{ParserError, StrContext}; - - use crate::ast::{Expr, Program, RecordDef, Type}; - use winnow::prelude::*; - use winnow::seq; - use winnow::stream::AsChar; - use winnow::token::{one_of, take_while}; - - fn ws<'a, F, O, E: ParserError<&'a str>>(inner: F) -> impl Parser<&'a str, O, E> - where - F: Parser<&'a str, O, E>, - { - delimited(multispace0, inner, multispace0) - } - - fn parse_ident(input: &mut &'_ str) -> PResult { - ws(( - one_of(|c: char| c.is_alpha() || c == '_'), - take_while(0.., |c: char| c.is_alphanum() || c == '_'), - ) - .recognize() - .verify(|i: &str| i != "in" && i != "new" && i != "letrec" && i != "record")) - .parse_next(input) - .map(|i| i.to_string()) - } - - fn parse_int(input: &mut &'_ str) -> PResult { - repeat( - 1.., - terminated(one_of('0'..='9'), repeat(0.., '_').map(|()| ())), - ) - .map(|()| ()) - .recognize() - .parse_next(input) - .map(|i| i.parse().expect("not an integer")) - } - - fn parse_type(input: &mut &'_ str) -> PResult { - ws(alt(( - "int".value(Type::Int), - parse_ident.map(Type::StructRef), - ))) - .parse_next(input) - } - - fn parse_field_def(input: &mut &'_ str) -> PResult<(String, Type)> { - seq!( - _: multispace0, - parse_ident, - _: ws(":"), - parse_type, - _: multispace0, - ) - .parse_next(input) - } - - fn parse_field_defs(input: &mut &'_ str) -> PResult> { - terminated(separated(0.., ws(parse_field_def), ws(",")), opt(ws(","))).parse_next(input) - } - - fn parse_field(input: &mut &'_ str) -> PResult<(String, Expr)> { - seq!( - _: multispace0, - parse_ident, - _: ws(":"), - parse_expr, - _: multispace0, - ) - .parse_next(input) - } - - fn parse_fields(input: &mut &'_ str) -> PResult> { - terminated(separated(0.., ws(parse_field), ws(",")), opt(ws(","))).parse_next(input) - } - - fn parse_item(input: &mut &'_ str) -> PResult { - seq! {RecordDef { - name: parse_ident, - // `_` fields are ignored when building the record - _: ws("{"), - fields: parse_field_defs, - _: ws("}"), - }} - .parse_next(input) - } - - fn parse_value(input: &mut &'_ str) -> PResult<(String, Expr)> { - seq!( - parse_ident, - _: ws("="), - parse_expr, - _: ws(";") - ) - .parse_next(input) - } - - fn parse_values(input: &mut &'_ str) -> PResult> { - repeat(0.., parse_value).parse_next(input) - } - - fn parse_basic_expr(input: &mut &'_ str) -> PResult { - alt(( - parse_int.map(Expr::Number), - parse_ident.map(Expr::Ident), - seq! { - _: ws("new"), - parse_ident, - // `_` fields are ignored when building the record - _: ws("{"), - parse_fields, - _: ws("}"), - } - .map(|(name, fields)| Expr::StructInit { name, fields }), - seq! { - _: ws("letrec"), - parse_values, - _: ws("in"), - parse_expr, - } - .map(|(values, in_expr)| Expr::LetRec { - values, - in_expr: Box::new(in_expr), - }), - seq! { - _: ws("("), - parse_expr, - _: ws(")"), - } - .map(|(i,)| i), - )) - .context(StrContext::Label("parse expr")) - .parse_next(input) - } - - fn parse_expr(input: &mut &'_ str) -> PResult { - let first = ws(parse_basic_expr).parse_next(input)?; - let mut res = repeat(0.., (ws("."), parse_ident).map(|(_, i)| i)).fold( - || first.clone(), - |acc, val| Expr::FieldAccess(Box::new(acc), val), - ); - - res.parse_next(input) - } - - enum ItemOrExpr { - Item(RecordDef), - Expr(Expr), - } - - pub(crate) fn parse(mut input: &str) -> PResult { - let mut items = Vec::new(); - let mut main = None; - - while !input.is_empty() { - match ws(alt(( - ws(preceded(ws("record"), parse_item.map(ItemOrExpr::Item))), - seq!( - _: ws("main"), - _: ws("="), - ws(parse_expr.map(ItemOrExpr::Expr)), - _: ws(";"), - ) - .map(|(i,)| i), - )) - .context(StrContext::Label("parse item"))) - .parse_next(&mut input)? - { - ItemOrExpr::Expr(e) => main = Some(e), - ItemOrExpr::Item(i) => items.push(i), - } - } - - Ok(Program { - record_types: items, - main: main.expect("no main"), - }) - } -} - -fn main() -> Result<(), Box> { - let example = parse::parse( - " -record A { - b: B, - x: int, -} -record B { - a: A, - x: int, -} - -main = letrec - a = new A {x: 4, b: b}; - b = new B {x: 3, a: a}; -in a.b.a.x; - - ", - ) - .map_err(|i| i.to_string())?; - - println!("Type of example is: {:?}", typecheck(&example)); - - Ok(()) -} -<<<<<<< HEAD - -#[cfg(test)] -mod tests { - use crate::{ast, parse::parse, typecheck}; - - fn test_example(program: &str, expected_main_type: ast::Type) { - let ast = parse(program).expect("parse failure"); - let ty = typecheck(&ast).expect("type not instantiated"); - assert_eq!(ty, expected_main_type) - } - - #[test] - fn test_integer() { - test_example(" main = 42; ", ast::Type::Int) - } -} -======= ->>>>>>> fe89878 (Also add tests for nightly, remove failing test) diff --git a/scopegraphs/examples/records/ast.rs b/scopegraphs/examples/records/ast.rs new file mode 100644 index 0000000..173b23d --- /dev/null +++ b/scopegraphs/examples/records/ast.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Type { + StructRef(String), + Int, +} + +#[derive(Debug)] +pub struct RecordDef { + pub name: String, + pub fields: HashMap, +} + +#[derive(Debug, Clone)] +pub enum Expr { + StructInit { + name: String, + fields: HashMap, + }, + #[allow(unused)] + Add(Box, Box), + Number(u64), + Ident(String), + FieldAccess(Box, String), + #[allow(unused)] + Let { + name: String, + value: Box, + in_expr: Box, + }, + LetRec { + values: HashMap, + in_expr: Box, + }, +} + +#[derive(Debug)] +pub struct Program { + /// Items can occur in any order. Like in Rust! + pub record_types: Vec, + pub main: Expr, +} diff --git a/scopegraphs/examples/records/main.rs b/scopegraphs/examples/records/main.rs new file mode 100644 index 0000000..a4b0a74 --- /dev/null +++ b/scopegraphs/examples/records/main.rs @@ -0,0 +1,424 @@ +use crate::ast::{Expr, Program, RecordDef, Type}; +use crate::queries::{resolve_lexical_ref, resolve_member_ref, resolve_record_ref}; +use async_recursion::async_recursion; +use futures::future::{join, join_all}; +use scopegraphs::completeness::FutureCompleteness; +use scopegraphs::RenderScopeData; +use scopegraphs::{Scope, ScopeGraph, Storage}; +use scopegraphs_macros::Label; +use smol::LocalExecutor; +use std::cell::RefCell; +use std::error::Error; +use std::fmt::Debug; +use std::fs::File; +use std::future::Future; +use std::rc::Rc; +use union_find::UnionFind; + +mod ast; +mod parse; +mod queries; +mod union_find; + +#[derive(Debug, Label, Copy, Clone, Hash, PartialEq, Eq)] +enum SgLabel { + TypeDefinition, + Definition, + Lexical, +} + +#[derive(Debug, Default, Hash, Eq, PartialEq, Clone)] +enum SgData { + VarDecl { + name: String, + ty: PartialType, + }, + TypeDecl { + name: String, + scope: Scope, + }, + + #[default] + Nothing, +} + +impl RenderScopeData for SgData { + fn render(&self) -> Option { + match self { + SgData::VarDecl { name, ty } => Some(format!("var {name}: {ty:?}")), + SgData::TypeDecl { name, scope } => Some(format!("record {name} -> {scope:?}")), + SgData::Nothing => None, + } + } +} + +impl SgData { + pub fn expect_var_decl(&self) -> &PartialType { + match self { + SgData::VarDecl { ty, .. } => ty, + _ => panic!("expected var decl, got {:?}", &self), + } + } + + pub fn expect_type_decl(&self) -> &Scope { + match self { + SgData::TypeDecl { scope: ty, .. } => ty, + _ => panic!("expected type decl, got {:?}", &self), + } + } +} + +/// A type variable, a placeholder for a type +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Copy)] +pub struct TypeVar(usize); + +/// A partial type can either still be a type variable, or a concrete type +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +enum PartialType { + /// A variable + Variable(TypeVar), + /// A record named `name` with a scope. + /// The scope contains the field of the record. + /// See "scopes as types" + Record { name: String, scope: Scope }, + /// A number type + Int, +} + +type RecordScopegraph<'sg> = ScopeGraph<'sg, SgLabel, SgData, FutureCompleteness>; + +struct TypeChecker<'sg, 'ex> { + sg: RecordScopegraph<'sg>, + uf: RefCell, + ex: LocalExecutor<'ex>, +} + +impl<'sg, 'ex> TypeChecker<'sg, 'ex> +where + 'sg: 'ex, +{ + fn spawn(self: &Rc, f: impl FnOnce(Rc) -> F) + where + F: Future + 'ex, + T: 'ex, + { + self.ex.spawn(f(self.clone())).detach() + } + + #[async_recursion(?Send)] + async fn typecheck_expr<'a>(self: Rc, ast: &'ex Expr, scope: Scope) -> PartialType { + match ast { + Expr::StructInit { name, fields } => { + let record_scope = resolve_record_ref(&self.sg, scope, name).await; + + // defer typechecking of all the fields.. + for (field_name, field_initializer) in fields { + self.spawn(|this| async move { + let (decl_type, init_type) = join( + resolve_member_ref(&this.sg, record_scope, field_name), + this.clone().typecheck_expr(field_initializer, scope), + ) + .await; + + this.uf.borrow_mut().union(decl_type, init_type); + }); + } + + // FIXME: field init exhaustiveness check omitted + + // .. but eagerly return the record type + PartialType::Record { + name: name.clone(), + scope: record_scope, + } + } + Expr::Add(l, r) => { + // type check left-hand-side asynchronously + self.spawn(|this| async move { + let l_ty = this.clone().typecheck_expr(l, scope).await; + this.uf.borrow_mut().union(l_ty, PartialType::Int); + }); + // and type-check the right-hand-side asynchronously + self.spawn(|this| async move { + let r_ty = this.clone().typecheck_expr(r, scope).await; + this.uf.borrow_mut().union(r_ty, PartialType::Int); + }); + + // ... but immediately return the current type + PartialType::Int + } + Expr::Number(_) => PartialType::Int, + Expr::Ident(var_name) => resolve_lexical_ref(&self.sg, scope, var_name).await, + Expr::FieldAccess(inner, field) => { + let res = self.clone().typecheck_expr(inner, scope).await; + let inner_expr_type = self.uf.borrow_mut().find_partial_type(res); + self.type_check_field_access(inner_expr_type, field).await + } + Expr::Let { + name, + value, + in_expr, + } => { + let new_scope = self + .sg + .add_scope_default_with([SgLabel::Lexical, SgLabel::Definition]); + self.sg + .add_edge(new_scope, SgLabel::Lexical, scope) + .expect("already closed"); + self.sg.close(new_scope, &SgLabel::Lexical); + + let ty_var = PartialType::Variable(self.uf.borrow_mut().fresh()); + self.sg + .add_decl( + new_scope, + SgLabel::Definition, + SgData::VarDecl { + name: name.clone(), + ty: ty_var.clone(), + }, + ) + .expect("already closed"); + + self.sg.close(new_scope, &SgLabel::Definition); + + self.spawn(|this| async move { + let ty = this.clone().typecheck_expr(value, scope).await; + this.uf.borrow_mut().union(ty_var, ty); + }); + + // compute type of the result expression + self.clone().typecheck_expr(in_expr, new_scope).await + } + Expr::LetRec { values, in_expr } => { + let new_scope = self + .sg + .add_scope_default_with([SgLabel::Lexical, SgLabel::Definition]); + self.sg + .add_edge(new_scope, SgLabel::Lexical, scope) + .expect("already closed"); + self.sg.close(new_scope, &SgLabel::Lexical); + + for (name, initializer_expr) in values { + let ty = PartialType::Variable(self.uf.borrow_mut().fresh()); + self.sg + .add_decl( + new_scope, + SgLabel::Definition, + SgData::VarDecl { + name: name.clone(), + ty: ty.clone(), + }, + ) + .expect("already closed"); + + self.spawn(|this| async move { + let init_ty = this + .clone() + .typecheck_expr(initializer_expr, new_scope) + .await; + this.uf.borrow_mut().union(ty, init_ty) + }); + } + self.sg.close(new_scope, &SgLabel::Definition); + + // compute type of the result expression + self.typecheck_expr(in_expr, new_scope).await + } + } + } + + async fn type_check_field_access( + self: Rc, + mut inner_expr_type: PartialType, + field: &str, + ) -> PartialType { + loop { + match inner_expr_type { + PartialType::Variable(tv) => { + let fut = self.uf.borrow_mut().wait_for_unification(tv); + inner_expr_type = fut.await; + } + PartialType::Record { scope, .. } => { + break resolve_member_ref(&self.sg, scope, field).await + } + PartialType::Int => panic!("number has no field {field}"), + } + } + } + + fn init_record_def(&self, record_def: &RecordDef, scope: Scope) -> Scope { + let field_scope = self.sg.add_scope_default_with([SgLabel::Definition]); + self.sg + .add_decl( + scope, + SgLabel::TypeDefinition, + SgData::TypeDecl { + name: record_def.name.clone(), + scope: field_scope, + }, + ) + .expect("already closed"); + self.sg.close(scope, &SgLabel::Definition); + + field_scope + } + + async fn typecheck_record_def( + self: Rc, + record_def: &RecordDef, + scope: Scope, + field_scope: Scope, + ) { + let fld_decl_futures = record_def.fields.iter().map(|(fld_name, fld_ty)| { + let this = self.clone(); + async move { + let ty = match fld_ty { + Type::StructRef(n) => { + let record_scope = resolve_record_ref(&this.sg, scope, n).await; + PartialType::Record { + name: n.clone(), + scope: record_scope, + } + } + Type::Int => PartialType::Int, + }; + + this.sg + .add_decl( + field_scope, + SgLabel::Definition, + SgData::VarDecl { + name: fld_name.clone(), + ty, + }, + ) + .expect("unexpected close"); + } + }); + + join_all(fld_decl_futures).await; + self.sg.close(field_scope, &SgLabel::Definition); + } +} + +fn typecheck(ast: &Program) -> Option { + let storage = Storage::new(); + let sg = RecordScopegraph::new(&storage, FutureCompleteness::default()); + let uf = RefCell::new(UnionFind::default()); + let local = LocalExecutor::new(); + + let tc = Rc::new(TypeChecker { sg, uf, ex: local }); + + let global_scope = tc.sg.add_scope_default_with([SgLabel::TypeDefinition]); + + // typecheck all the type definitions somewhere in the future + for item in &ast.record_types { + // synchronously init record decl + let field_scope = tc.init_record_def(item, global_scope); + tc.spawn(|this| this.typecheck_record_def(item, global_scope, field_scope)); + } + + // We can close for type definitions since the scopes for this are synchronously + // made even before the future is returned and spawned. so, at this point, + // no new type definitions are made. + tc.sg.close(global_scope, &SgLabel::TypeDefinition); + + // typecheck the main expression + let res = tc + .ex + .spawn(tc.clone().typecheck_expr(&ast.main, global_scope)); + + // extract result from task + let main_ty = smol::block_on(async { + while !tc.ex.is_empty() { + tc.ex.tick().await; + } + + res.await + }); + + tc.sg + .render(&mut File::create("sg.dot").unwrap(), "sg") + .unwrap(); + println!("{:?}", tc.uf.borrow()); + + let resolved_main_ty = tc.uf.borrow_mut().type_of_partial_type(main_ty); + resolved_main_ty +} + +fn main() -> Result<(), Box> { + let example = parse::parse( + " + ", + ) + .map_err(|i| i.to_string())?; + + println!("Type of example is: {:?}", typecheck(&example)); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{ast, parse::parse, typecheck}; + + fn test_example(program: &str, expected_main_type: ast::Type) { + let ast = parse(program).expect("parse failure"); + let ty = typecheck(&ast).expect("type not instantiated"); + assert_eq!(ty, expected_main_type) + } + + #[test] + fn test_integer() { + test_example("main = 42;", ast::Type::Int) + } + + #[test] + fn test_letrec() { + test_example("main = letrec a = 42; in a;", ast::Type::Int) + } + + #[test] + fn test_let() { + test_example("main = let a = 42; in a;", ast::Type::Int) + } + + #[test] + fn test_shadow() { + test_example( + "record A {} main = let a = new A {}; in let a = 42; in a;", + ast::Type::Int, + ) + } + + #[test] + #[should_panic = "duplicate definition"] + fn test_letrec_shadow() { + test_example( + "record A {} main = letrec a = new A {}; a = 42; in a;", + ast::Type::Int, + ) + } + + #[test] + fn test_complex() { + test_example( + " +record A { + b: B, + x: int, +} +record B { + a: A, + x: int, +} + +main = letrec + a = new A {x: 4, b: b}; + b = new B {x: 3, a: a}; +in a.b.a.x; + ", + ast::Type::Int, + ) + } +} diff --git a/scopegraphs/examples/records/parse.rs b/scopegraphs/examples/records/parse.rs new file mode 100644 index 0000000..bfbecb5 --- /dev/null +++ b/scopegraphs/examples/records/parse.rs @@ -0,0 +1,191 @@ +use std::collections::HashMap; +use winnow::ascii::multispace0; +use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated, terminated}; +use winnow::error::{ParserError, StrContext}; + +use crate::ast::{Expr, Program, RecordDef, Type}; +use winnow::prelude::*; +use winnow::seq; +use winnow::stream::AsChar; +use winnow::token::{one_of, take_while}; + +fn ws<'a, F, O, E: ParserError<&'a str>>(inner: F) -> impl Parser<&'a str, O, E> +where + F: Parser<&'a str, O, E>, +{ + delimited(multispace0, inner, multispace0) +} + +fn parse_ident(input: &mut &'_ str) -> PResult { + ws(( + one_of(|c: char| c.is_alpha() || c == '_'), + take_while(0.., |c: char| c.is_alphanum() || c == '_'), + ) + .recognize() + .verify(|i: &str| !["in", "new", "letrec", "record", "let"].contains(&i))) + .parse_next(input) + .map(|i| i.to_string()) +} + +fn parse_int(input: &mut &'_ str) -> PResult { + repeat( + 1.., + terminated(one_of('0'..='9'), repeat(0.., '_').map(|()| ())), + ) + .map(|()| ()) + .recognize() + .parse_next(input) + .map(|i| i.parse().expect("not an integer")) +} + +fn parse_type(input: &mut &'_ str) -> PResult { + ws(alt(( + "int".value(Type::Int), + parse_ident.map(Type::StructRef), + ))) + .parse_next(input) +} + +fn parse_field_def(input: &mut &'_ str) -> PResult<(String, Type)> { + seq!( + _: multispace0, + parse_ident, + _: ws(":"), + parse_type, + _: multispace0, + ) + .parse_next(input) +} + +fn parse_field_defs(input: &mut &'_ str) -> PResult> { + terminated(separated(0.., ws(parse_field_def), ws(",")), opt(ws(","))).parse_next(input) +} + +fn parse_field(input: &mut &'_ str) -> PResult<(String, Expr)> { + seq!( + _: multispace0, + parse_ident, + _: ws(":"), + parse_expr, + _: multispace0, + ) + .parse_next(input) +} + +fn parse_fields(input: &mut &'_ str) -> PResult> { + terminated(separated(0.., ws(parse_field), ws(",")), opt(ws(","))).parse_next(input) +} + +fn parse_item(input: &mut &'_ str) -> PResult { + seq! {RecordDef { + name: parse_ident, + // `_` fields are ignored when building the record + _: ws("{"), + fields: parse_field_defs, + _: ws("}"), + }} + .parse_next(input) +} + +fn parse_value(input: &mut &'_ str) -> PResult<(String, Expr)> { + seq!( + parse_ident, + _: ws("="), + parse_expr, + _: ws(";") + ) + .parse_next(input) +} + +fn parse_values(input: &mut &'_ str) -> PResult> { + repeat(0.., parse_value).parse_next(input) +} + +fn parse_basic_expr(input: &mut &'_ str) -> PResult { + alt(( + parse_int.map(Expr::Number), + parse_ident.map(Expr::Ident), + seq! { + _: ws("new"), + parse_ident, + // `_` fields are ignored when building the record + _: ws("{"), + parse_fields, + _: ws("}"), + } + .map(|(name, fields)| Expr::StructInit { name, fields }), + seq! { + _: ws("letrec"), + parse_values, + _: ws("in"), + parse_expr, + } + .map(|(values, in_expr)| Expr::LetRec { + values, + in_expr: Box::new(in_expr), + }), + seq! { + _: ws("let"), + parse_value, + _: ws("in"), + parse_expr, + } + .map(|((name, value), in_expr)| Expr::Let { + name, + value: Box::new(value), + in_expr: Box::new(in_expr), + }), + seq! { + _: ws("("), + parse_expr, + _: ws(")"), + } + .map(|(i,)| i), + )) + .context(StrContext::Label("parse expr")) + .parse_next(input) +} + +fn parse_expr(input: &mut &'_ str) -> PResult { + let first = ws(parse_basic_expr).parse_next(input)?; + let mut res = repeat(0.., (ws("."), parse_ident).map(|(_, i)| i)).fold( + || first.clone(), + |acc, val| Expr::FieldAccess(Box::new(acc), val), + ); + + res.parse_next(input) +} + +enum ItemOrExpr { + Item(RecordDef), + Expr(Expr), +} + +pub fn parse(mut input: &str) -> PResult { + let mut items = Vec::new(); + let mut main = None; + + while !input.is_empty() { + match ws(alt(( + ws(preceded(ws("record"), parse_item.map(ItemOrExpr::Item))), + seq!( + _: ws("main"), + _: ws("="), + ws(parse_expr.map(ItemOrExpr::Expr)), + _: ws(";"), + ) + .map(|(i,)| i), + )) + .context(StrContext::Label("parse item"))) + .parse_next(&mut input)? + { + ItemOrExpr::Expr(e) => main = Some(e), + ItemOrExpr::Item(i) => items.push(i), + } + } + + Ok(Program { + record_types: items, + main: main.expect("no main"), + }) +} diff --git a/scopegraphs/examples/records/queries.rs b/scopegraphs/examples/records/queries.rs new file mode 100644 index 0000000..d7dcbb6 --- /dev/null +++ b/scopegraphs/examples/records/queries.rs @@ -0,0 +1,70 @@ +use crate::{PartialType, RecordScopegraph, SgData, SgLabel}; +use scopegraphs::resolve::Resolve; +use scopegraphs::{query_regex, Scope}; +use scopegraphs_macros::label_order; + +pub async fn resolve_record_ref(sg: &RecordScopegraph<'_>, scope: Scope, ref_name: &str) -> Scope { + let env = sg + .query() + .with_path_wellformedness(query_regex!(SgLabel: Lexical* TypeDefinition)) + .with_data_wellformedness(|record_data: &SgData| match record_data { + SgData::TypeDecl { + name: decl_name, .. + } => decl_name == ref_name, + _ => false, + }) + .with_label_order(label_order!(SgLabel: Definition < Lexical)) + .resolve(scope) + .await; + + *env.get_only_item() + .expect("record name did not resolve properly") + .data() + .expect_type_decl() +} + +pub async fn resolve_lexical_ref( + sg: &RecordScopegraph<'_>, + scope: Scope, + var_name: &str, +) -> PartialType { + let env = sg + .query() + .with_path_wellformedness(query_regex!(SgLabel: Lexical* Definition)) + .with_label_order(label_order!(SgLabel: Definition < Lexical)) + .with_data_wellformedness(|record_data: &SgData| -> bool { + matches!(record_data, SgData::VarDecl { name, .. } if name == var_name) + }) + .resolve(scope) + .await; + + env.get_only_item() + .expect("variable did not resolve uniquely") + .data() + .expect_var_decl() + .clone() +} + +pub async fn resolve_member_ref( + sg: &RecordScopegraph<'_>, + record_scope: Scope, + ref_name: &str, +) -> PartialType { + let env = sg + .query() + .with_path_wellformedness(query_regex!(SgLabel: Definition)) + .with_data_wellformedness(|record_data: &SgData| match record_data { + SgData::VarDecl { + name: decl_name, .. + } => decl_name == ref_name, + _ => false, + }) + .resolve(record_scope) + .await; + + env.get_only_item() + .expect("field name did not resolve properly") + .data() + .expect_var_decl() + .clone() +} diff --git a/scopegraphs/examples/records/tests.rs b/scopegraphs/examples/records/tests.rs new file mode 100644 index 0000000..883c6f2 --- /dev/null +++ b/scopegraphs/examples/records/tests.rs @@ -0,0 +1,44 @@ +use crate::{ast, parse::parse, typecheck}; + +fn test_example(program: &str, expected_main_type: ast::Type) { + let ast = parse(program).expect("parse failure"); + let ty = typecheck(&ast).expect("type not instantiated"); + assert_eq!(ty, expected_main_type) +} + +#[test] +fn test_integer() { + test_example("main = 42;", ast::Type::Int) +} + +#[test] +fn test_letrec() { + test_example("main = letrec a = 42; in a;", ast::Type::Int) +} + +#[test] +fn test_let() { + test_example("main = let a = 42; in a;", ast::Type::Int) +} + +#[test] +fn test_complex() { + test_example( + " +record A { + b: B, + x: int, +} +record B { + a: A, + x: int, +} + +main = letrec + a = new A {x: 4, b: b}; + b = new B {x: 3, a: a}; +in a.b.a.x; + ", + ast::Type::Int, + ) +} diff --git a/scopegraphs/examples/records/union_find.rs b/scopegraphs/examples/records/union_find.rs new file mode 100644 index 0000000..520cc1b --- /dev/null +++ b/scopegraphs/examples/records/union_find.rs @@ -0,0 +1,153 @@ +use std::fmt::{Debug, Formatter}; + +use futures::Future; +use smol::channel::{bounded, Sender}; + +use crate::{ast::Type, PartialType, TypeVar}; + +#[derive(Default)] +pub struct UnionFind { + /// Records the parent of each type variable. + /// Kind of assumes type variables are assigned linearly. + /// + /// For example the "parent" of type variable 0 is stored at index 0 + parent: Vec, + /// Keep track of type variables we've given out + vars: usize, + /// A vec of signals for each type variable. + /// + /// For example, whenever type variable 0 is unified with anything, + /// we go through the list at index 0 and notify each. + callbacks: Vec>>, +} + +impl Debug for UnionFind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{{")?; + for (idx, p) in self.parent.iter().enumerate() { + write!(f, "{idx} -> {p:?}")?; + if (idx + 1) < self.parent.len() { + write!(f, ", ")?; + } + } + write!(f, "}}") + } +} + +impl UnionFind { + /// Create a new type variable + /// (which happens to be one bigger than the previous fresh type variable) + pub fn fresh(&mut self) -> TypeVar { + let old = self.vars; + self.vars += 1; + + TypeVar(old) + } + + /// Unify two partial types, asserting they are equal to each other. + /// + /// If one of left or right is a concrete type, and the other is a type variable, + /// we've essentially resolved what type the type variable is now, and we update the + /// data structure to represent that. The next [`find`](Self::find) of this type variable + /// will return the concrete type after this unification. + /// + /// Sometimes, two type variables are unified. In that case, one of the two is chosen by + /// a fair (trust me) dice roll and is made the representative of both input type variables. + /// Whenever one of the two is now unified with a concrete type, both input type variables + /// become equal to that concrete type. + pub fn union(&mut self, left: PartialType, right: PartialType) { + let left = self.find_partial_type(left); + let right = self.find_partial_type(right); + + match (left, right) { + (PartialType::Variable(left), right) | (right, PartialType::Variable(left)) => { + // FIXME: use rank heuristic in case right is a variable? + *self.get(left) = right.clone(); + if self.callbacks.len() > left.0 { + for fut in self.callbacks[left.0].drain(..) { + let _ = fut.send_blocking(right.clone()); + } + } + } + (left, right) if left != right => { + panic!("type error: cannot unify {left:?} and {right:?}"); + } + _ => {} + } + } + + /// Find the representative for a given type variable. + /// In the best case, this is a concrete type this type variable is equal to. + /// That's nice, because now we know what that type variable was supposed to be. + /// + /// However, it's possible we find another type variable instead (wrapped in a [`PartialType`]). + /// Now we know that this new type variable has the same type of the given type variable, + /// we just don't know yet which type that is. More unifications are needed. + fn find(&mut self, ty: TypeVar) -> PartialType { + let res = self.get(ty); + if let PartialType::Variable(v) = *res { + if v == ty { + return PartialType::Variable(ty); + } + + // do path compression + let root = self.find(v); + *self.get(v) = root.clone(); + root + } else { + res.clone() + } + } + + /// [find](Self::find), but for a parial type + pub fn find_partial_type(&mut self, ty: PartialType) -> PartialType { + if let PartialType::Variable(v) = ty { + self.find(v) + } else { + ty + } + } + + /// Get a mutable reference to parent of a given type variable. + /// Used in the implementation of [`find`](Self::find) and [`union`](Self::union) + fn get(&mut self, tv: TypeVar) -> &mut PartialType { + let parent = &mut self.parent; + for i in parent.len()..=tv.0 { + parent.push(PartialType::Variable(TypeVar(i))); + } + + &mut parent[tv.0] + } + + #[allow(unused)] + fn type_of(&mut self, var: TypeVar) -> Option { + match self.find(var) { + PartialType::Variable(_) => None, + PartialType::Record { name, .. } => Some(Type::StructRef(name)), + PartialType::Int => Some(Type::Int), + } + } + + pub fn type_of_partial_type(&mut self, var: PartialType) -> Option { + match self.find_partial_type(var) { + PartialType::Variable(_) => None, + PartialType::Record { name, .. } => Some(Type::StructRef(name)), + PartialType::Int => Some(Type::Int), + } + } + + /// Wait for when tv is unified with something. + pub fn wait_for_unification(&mut self, tv: TypeVar) -> impl Future { + let callbacks = &mut self.callbacks; + for _ in callbacks.len()..=tv.0 { + callbacks.push(vec![]); + } + + let (tx, rx) = bounded(1); + callbacks[tv.0].push(tx); + + // not an async function, cause when we await this we don't want to hold on to a &mut self. + // This future can complete on its own. + async move { rx.recv().await.expect("sender dropped") } + } +} From 30c31330d55494fd8b05ed03297c98e8283339ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Mon, 27 May 2024 22:04:42 +0200 Subject: [PATCH 7/7] fix duplicate definition test --- scopegraphs/examples/records/ast.rs | 8 ++--- scopegraphs/examples/records/main.rs | 2 +- scopegraphs/examples/records/parse.rs | 7 ++--- scopegraphs/examples/records/tests.rs | 44 --------------------------- 4 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 scopegraphs/examples/records/tests.rs diff --git a/scopegraphs/examples/records/ast.rs b/scopegraphs/examples/records/ast.rs index 173b23d..aa4d795 100644 --- a/scopegraphs/examples/records/ast.rs +++ b/scopegraphs/examples/records/ast.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum Type { StructRef(String), @@ -9,14 +7,14 @@ pub enum Type { #[derive(Debug)] pub struct RecordDef { pub name: String, - pub fields: HashMap, + pub fields: Vec<(String, Type)>, } #[derive(Debug, Clone)] pub enum Expr { StructInit { name: String, - fields: HashMap, + fields: Vec<(String, Expr)>, }, #[allow(unused)] Add(Box, Box), @@ -30,7 +28,7 @@ pub enum Expr { in_expr: Box, }, LetRec { - values: HashMap, + values: Vec<(String, Expr)>, in_expr: Box, }, } diff --git a/scopegraphs/examples/records/main.rs b/scopegraphs/examples/records/main.rs index a4b0a74..cd9dfc5 100644 --- a/scopegraphs/examples/records/main.rs +++ b/scopegraphs/examples/records/main.rs @@ -392,7 +392,7 @@ mod tests { } #[test] - #[should_panic = "duplicate definition"] + #[should_panic = "variable did not resolve uniquely: OnlyElementError::Multiple {..}"] fn test_letrec_shadow() { test_example( "record A {} main = letrec a = new A {}; a = 42; in a;", diff --git a/scopegraphs/examples/records/parse.rs b/scopegraphs/examples/records/parse.rs index bfbecb5..03b959c 100644 --- a/scopegraphs/examples/records/parse.rs +++ b/scopegraphs/examples/records/parse.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use winnow::ascii::multispace0; use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated, terminated}; use winnow::error::{ParserError, StrContext}; @@ -57,7 +56,7 @@ fn parse_field_def(input: &mut &'_ str) -> PResult<(String, Type)> { .parse_next(input) } -fn parse_field_defs(input: &mut &'_ str) -> PResult> { +fn parse_field_defs(input: &mut &'_ str) -> PResult> { terminated(separated(0.., ws(parse_field_def), ws(",")), opt(ws(","))).parse_next(input) } @@ -72,7 +71,7 @@ fn parse_field(input: &mut &'_ str) -> PResult<(String, Expr)> { .parse_next(input) } -fn parse_fields(input: &mut &'_ str) -> PResult> { +fn parse_fields(input: &mut &'_ str) -> PResult> { terminated(separated(0.., ws(parse_field), ws(",")), opt(ws(","))).parse_next(input) } @@ -97,7 +96,7 @@ fn parse_value(input: &mut &'_ str) -> PResult<(String, Expr)> { .parse_next(input) } -fn parse_values(input: &mut &'_ str) -> PResult> { +fn parse_values(input: &mut &'_ str) -> PResult> { repeat(0.., parse_value).parse_next(input) } diff --git a/scopegraphs/examples/records/tests.rs b/scopegraphs/examples/records/tests.rs deleted file mode 100644 index 883c6f2..0000000 --- a/scopegraphs/examples/records/tests.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::{ast, parse::parse, typecheck}; - -fn test_example(program: &str, expected_main_type: ast::Type) { - let ast = parse(program).expect("parse failure"); - let ty = typecheck(&ast).expect("type not instantiated"); - assert_eq!(ty, expected_main_type) -} - -#[test] -fn test_integer() { - test_example("main = 42;", ast::Type::Int) -} - -#[test] -fn test_letrec() { - test_example("main = letrec a = 42; in a;", ast::Type::Int) -} - -#[test] -fn test_let() { - test_example("main = let a = 42; in a;", ast::Type::Int) -} - -#[test] -fn test_complex() { - test_example( - " -record A { - b: B, - x: int, -} -record B { - a: A, - x: int, -} - -main = letrec - a = new A {x: 4, b: b}; - b = new B {x: 3, a: a}; -in a.b.a.x; - ", - ast::Type::Int, - ) -}