Last active
July 17, 2024 01:13
-
-
Save brendanzab/6c562b12b51c424f3a93356faeb106ed to your computer and use it in GitHub Desktop.
A small dependent type system, implemented in SWI-Prolog using normalisation-by-evaluation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| :- module(core, | |
| [ % Operational semantics | |
| eval/2, % +Expr, -VExpr | |
| quote/2, % +VExpr, -Expr | |
| normalise/2, % +Term, -Term | |
| eval/3, % +Env, +Expr, -VExpr | |
| quote/3, % +Len, +VExpr, -Expr | |
| normalise/3, % +Env, +Term, -Term | |
| convert/3, % +Len, +VExpr, +VExpr | |
| % Bidirectional typing | |
| to_env/2, % +Ctx, -Env | |
| check/2, % +Expr, +VType | |
| synth/2, % +Expr, -VType | |
| check/3, % +Ctx, +Expr, +VType | |
| synth/3 % +Ctx, +Expr, -VType | |
| ]). | |
| :- use_module(library(lists), []). | |
| %% type index == nonneg. | |
| %% type level == nonneg. | |
| %% type term ---> | |
| %% var(index); | |
| %% let(term, term, term); | |
| %% type; | |
| %% fun_type(term, term); | |
| %% fun_lit(term); | |
| %% fun_call(term, term); | |
| %% pair_type(term, term); | |
| %% pair_lit(term, term); | |
| %% pair_fst(term); | |
| %% pair_snd(term). | |
| %% type value ---> | |
| %% neutral(neutral); | |
| %% type; | |
| %% fun_type(value, closure); | |
| %% fun_lit(closure); | |
| %% pair_type(value, closure); | |
| %% pair_lit(value, value); | |
| %% unit_type; | |
| %% unit_lit. | |
| %% type neutral ---> | |
| %% var(level); | |
| %% fun_call(neutral, value); | |
| %% pair_fst(neutral); | |
| %% pair_snd(neutral). | |
| %% type env == list(value). | |
| %% type closure ---> closure(Env, Expr). | |
| %% type context == list(value-value). | |
| % -------------------------------------------------------------------------------------------------- | |
| % | |
| % Operational semantics | |
| % | |
| % -------------------------------------------------------------------------------------------------- | |
| %% eval(+Expr:term, -VExpr:value) is semidet. | |
| eval(Expr, VExpr) :- | |
| eval([], Expr, VExpr). | |
| %% quote(+VExpr:value, -Expr:term) is semidet. | |
| quote(VExpr, Expr) :- | |
| quote(0, VExpr, Expr). | |
| %% normalise(+Term:term, -Term:term) is semidet. | |
| normalise(Term1, Term2) :- | |
| normalise([], Term1, Term2). | |
| %% eval(+Env:env, +Expr:term, -VExpr:value) is semidet. | |
| eval(Env, var(Index), VExpr) :- | |
| lists:nth0(Index, Env, VExpr). | |
| eval(Env, let(_, ExprDef, Expr), VExpr) :- | |
| eval(Env, ExprDef, VExprDef), | |
| eval([VExprDef | Env], Expr, VExpr). | |
| eval(_, type, type). | |
| eval(Env, fun_type(TypeIn, TypeOut), fun_type(VTypeIn, closure(Env, TypeOut))) :- | |
| eval(Env, TypeIn, VTypeIn). | |
| eval(Env, fun_lit(ExprOut), fun_lit(closure(Env, ExprOut))). | |
| eval(Env, fun_call(Expr, ExprIn), VTypeOut) :- | |
| eval(Env, Expr, VExpr), | |
| eval(Env, ExprIn, VExprIn), | |
| compute_call(VExpr, VExprIn, VTypeOut). | |
| eval(Env, pair_type(TypeFst, TypeSnd), pair_type(VTypeFst, closure(Env, TypeSnd))) :- | |
| eval(Env, TypeFst, VTypeFst). | |
| eval(Env, pair_lit(ExprFst, ExprSnd), pair_lit(VExprFst, VExprSnd)) :- | |
| eval(Env, ExprFst, VExprFst), | |
| eval(Env, ExprSnd, VExprSnd). | |
| eval(Env, pair_fst(Expr), VExprFst) :- | |
| eval(Env, Expr, VExpr), | |
| compute_fst(VExpr, VExprFst). | |
| eval(Env, pair_snd(Expr), VExprSnd) :- | |
| eval(Env, Expr, VExpr), | |
| compute_snd(VExpr, VExprSnd). | |
| %% apply(+ClosExpr:closure, +VExpr:value, -VExpr:value) is semidet. | |
| apply(closure(Env, Term), VExpr1, VExpr2) :- | |
| eval([VExpr1 | Env], Term, VExpr2). | |
| %% compute_call(+VExpr:value, +VExpr:value, -VExpr:value) is semidet. | |
| compute_call(fun_lit(ClosExprOut), VExprIn, VExprOut) :- | |
| apply(ClosExprOut, VExprIn, VExprOut). | |
| compute_call(neutral(NeuExpr), VExprIn, neutral(fun_call(NeuExpr, VExprIn))). | |
| %% compute_fst(+VExpr:value, -VExpr:value) is semidet. | |
| compute_fst(pair_lit(VExprFst, _), VExprFst). | |
| compute_fst(neutral(NeuExpr), neutral(pair_fst(NeuExpr))). | |
| %% compute_snd(+VExpr:value, -VExpr:value) is semidet. | |
| compute_snd(pair_lit(_, VExprSnd), VExprSnd). | |
| compute_snd(neutral(NeuExpr), neutral(pair_snd(NeuExpr))). | |
| %% quote(+Len:nonneg, +VExpr:value, -Expr:term) is semidet. | |
| quote(Len, neutral(NeuExpr), Expr) :- | |
| quote_neutral(Len, NeuExpr, Expr). | |
| quote(_, type, type). | |
| quote(Len, fun_type(VTypeIn, ClosTypeOut), fun_type(TypeIn, TypeOut)) :- | |
| quote(Len, VTypeIn, TypeIn), | |
| apply(ClosTypeOut, neutral(var(Len)), VTypeOut), | |
| quote(Len + 1, VTypeOut, TypeOut). | |
| quote(Len, fun_lit(ClosExprOut), fun_lit(ExprOut)) :- | |
| apply(ClosExprOut, neutral(var(Len)), VExprOut), | |
| quote(Len + 1, VExprOut, ExprOut). | |
| quote(Len, pair_type(VTypeFst, ClosTypeSnd), pair_type(TypeFst, TypeSnd)) :- | |
| quote(Len, VTypeFst, TypeFst), | |
| apply(ClosTypeSnd, neutral(var(Len)), VTypeSnd), | |
| quote(Len + 1, VTypeSnd, TypeSnd). | |
| quote(Len, pair_lit(VExprFst, VExprSnd), pair_lit(ExprFst, ExprSnd)) :- | |
| quote(Len, VExprFst, ExprFst), | |
| quote(Len, VExprSnd, ExprSnd). | |
| %% quote_neutral(+Len:nonneg, +NeuExpr:neutral, -Expr:term) is semidet. | |
| quote_neutral(Len, var(Level), var(Len - Level + 1)). | |
| quote_neutral(Len, fun_call(NeuExpr1, VExpr2), fun_call(Expr1, Expr2)) :- | |
| quote_neutral(Len, NeuExpr1, Expr1), | |
| quote(Len, VExpr2, Expr2). | |
| quote_neutral(Len, pair_fst(NeuExpr), pair_fst(Expr)) :- | |
| quote_neutral(Len, NeuExpr, Expr). | |
| quote_neutral(Len, pair_snd(NeuExpr), pair_snd(Expr)) :- | |
| quote_neutral(Len, NeuExpr, Expr). | |
| %% normalise(+Env:env, +Term1:term, -Term2:term) is semidet. | |
| normalise(Env, Term1, Term2) :- | |
| eval(Env, Term1, VTerm1), | |
| lists:length(Env, Len), | |
| quote(Len, VTerm1, Term2). | |
| %% convert(+Len:nonneg, +VExpr1:value, +VExpr2:value) is semidet. | |
| convert(Len, neutral(NeuExpr1), neutral(NeuExpr2)) :- | |
| convert_neutral(Len, NeuExpr1, NeuExpr2). | |
| convert(_, type, type). | |
| convert(Len, fun_type(VTypeIn1, ClosTypeOut1), fun_type(VTypeIn2, ClosTypeOut2)) :- | |
| convert(Len, VTypeIn1, VTypeIn2), | |
| apply(ClosTypeOut1, neutral(var(Len)), VTypeOut1), | |
| apply(ClosTypeOut2, neutral(var(Len)), VTypeOut2), | |
| convert(Len + 1, VTypeOut1, VTypeOut2). | |
| convert(Len, fun_lit(ClosExprOut1), fun_lit(ClosExprOut2)) :- | |
| apply(ClosExprOut1, neutral(var(Len)), VExprOut1), | |
| apply(ClosExprOut2, neutral(var(Len)), VExprOut2), | |
| convert(Len + 1, VExprOut1, VExprOut2). | |
| convert(Len, VExpr1, fun_lit(ClosExprOut2)) :- | |
| compute_call(VExpr1, neutral(var(Len)), VExprOut1), | |
| apply(ClosExprOut2, neutral(var(Len)), VExprOut2), | |
| convert(Len, VExprOut1, VExprOut2). | |
| convert(Len, fun_lit(ClosExprOut1), VExpr2) :- | |
| apply(ClosExprOut1, neutral(var(Len)), VExprOut1), | |
| compute_call(VExpr2, neutral(var(Len)), VExprOut2), | |
| convert(Len, VExprOut1, VExprOut2). | |
| convert(Len, pair_type(VTypeFst1, ClosTypeSnd1), pair_type(VTypeFst2, ClosTypeSnd2)) :- | |
| convert(Len, VTypeFst1, VTypeFst2), | |
| apply(ClosTypeSnd1, neutral(var(Len)), VTypeSnd1), | |
| apply(ClosTypeSnd2, neutral(var(Len)), VTypeSnd2), | |
| convert(Len + 1, VTypeSnd1, VTypeSnd2). | |
| convert(Len, pair_lit(VExprFst1, VExprSnd1), pair_lit(VExprFst2, VExprSnd2)) :- | |
| convert(Len, VExprFst1, VExprFst2), | |
| convert(Len, VExprSnd1, VExprSnd2). | |
| convert(Len, VExpr1, pair_lit(VExprFst2, VExprSnd2)) :- | |
| compute_fst(VExpr1, VExprFst1), | |
| convert(Len, VExprFst1, VExprFst2), | |
| compute_snd(VExpr1, VExprSnd1), | |
| convert(Len, VExprSnd1, VExprSnd2). | |
| convert(Len, pair_lit(VExprFst1, VExprSnd1), VExpr2) :- | |
| compute_fst(VExpr2, VExprFst2), | |
| convert(Len, VExprFst1, VExprFst2), | |
| compute_snd(VExpr2, VExprSnd2), | |
| convert(Len, VExprSnd1, VExprSnd2). | |
| %% convert_neutral(+Len:nonneg, +NeuExpr1:neutral, +NeuExpr2:neutral) is semidet. | |
| convert_neutral(_, var(Level1), var(Level2)) :- Level1 =:= Level2. | |
| convert_neutral(Len, fun_call(NeuExpr1, VExprIn1), fun_call(NeuExpr2, VExprIn2)) :- | |
| convert_neutral(Len, NeuExpr1, NeuExpr2), | |
| convert(Len, VExprIn1, VExprIn2). | |
| convert_neutral(Len, pair_fst(NeuExpr1), pair_fst(NeuExpr2)) :- | |
| convert_neutral(Len, NeuExpr1, NeuExpr2). | |
| convert_neutral(Len, pair_snd(NeuExpr1), pair_snd(NeuExpr2)) :- | |
| convert_neutral(Len, NeuExpr1, NeuExpr2). | |
| % -------------------------------------------------------------------------------------------------- | |
| % | |
| % Bidirectional typing | |
| % | |
| % -------------------------------------------------------------------------------------------------- | |
| %% to_env(+Ctx:context, -Env:env) is semidet. | |
| to_env([], []). | |
| to_env([Expr-_ | Ctx], [Expr | Env]) :- | |
| to_env(Ctx, Env). | |
| %% check(+Expr:term, +VType:value) is semidet. | |
| check(Expr, VType) :- | |
| check([], Expr, VType). | |
| %% synth(+Expr:term, -VType:value) is semidet. | |
| synth(Expr, VType) :- | |
| synth([], Expr, VType). | |
| %% check(+Ctx:context, +Expr:term, +VType:value) is semidet. | |
| check(Ctx, let(TypeDef, ExprDef, Expr), VType) :- | |
| check(Ctx, TypeDef, type), | |
| to_env(Ctx, Env), | |
| eval(Env, TypeDef, VTypeDef), | |
| check(Ctx, ExprDef, VTypeDef), | |
| eval(Env, ExprDef, VExprDef), | |
| check([VExprDef-VTypeDef | Ctx], Expr, VType). | |
| check(Ctx, fun_lit(OutExpr), fun_type(VTypeIn, ClosTypeOut)) :- | |
| lists:length(Ctx, Len), | |
| apply(ClosTypeOut, neutral(var(Len)), VTypeOut), | |
| check([neutral(var(Len))-VTypeIn | Ctx], OutExpr, VTypeOut). | |
| check(Ctx, pair_lit(ExprFst, ExprSnd), pair_type(VTypeFst, ClosTypeSnd)) :- | |
| check(Ctx, ExprFst, VTypeFst), | |
| to_env(Ctx, Env), | |
| eval(Env, ExprFst, VExprFst), | |
| apply(ClosTypeSnd, VExprFst, VTypeSnd), | |
| check([VExprFst-VTypeFst | Ctx], ExprSnd, VTypeSnd). | |
| check(Ctx, Expr, VType2) :- | |
| synth(Ctx, Expr, VType1), | |
| lists:length(Ctx, Len), | |
| convert(Len, VType1, VType2). | |
| %% synth(+Ctx:context, +Expr:term, -VType:value) is semidet. | |
| synth(Ctx, var(Index), VType) :- | |
| lists:nth0(Index, Ctx, _-VType). | |
| synth(Ctx, let(TypeDef, ExprDef, Expr), VType) :- | |
| check(Ctx, TypeDef, type), | |
| to_env(Ctx, Env), | |
| eval(Env, TypeDef, VTypeDef), | |
| check(Ctx, ExprDef, VTypeDef), | |
| eval(Env, ExprDef, VExprDef), | |
| synth([VExprDef-VTypeDef | Ctx], Expr, VType). | |
| synth(_, type, type). | |
| synth(Ctx, fun_type(TypeIn, TypeOut), type) :- | |
| check(Ctx, TypeIn, type), | |
| to_env(Ctx, Env), | |
| eval(Env, TypeIn, VTypeIn), | |
| lists:length(Ctx, Len), | |
| check([neutral(var(Len))-VTypeIn | Ctx], TypeOut, type). | |
| synth(Ctx, fun_call(Expr, ExprIn), VTypeOut) :- | |
| synth(Ctx, Expr, fun_type(VTypeIn, ClosTypeOut)), | |
| check(Ctx, ExprIn, VTypeIn), | |
| to_env(Ctx, Env), | |
| eval(Env, ExprIn, VExprIn), | |
| apply(ClosTypeOut, VExprIn, VTypeOut). | |
| synth(Ctx, pair_type(TypeFst, TypeSnd), type) :- | |
| check(Ctx, TypeFst, type), | |
| to_env(Ctx, Env), | |
| eval(Env, TypeFst, VTypeFst), | |
| lists:length(Ctx, Len), | |
| check([neutral(var(Len))-VTypeFst | Ctx], TypeSnd, type). | |
| synth(Ctx, pair_fst(Expr), VTypeFst) :- | |
| synth(Ctx, Expr, pair_type(VTypeFst, _)). | |
| synth(Ctx, pair_snd(Expr), VTypeSnd) :- | |
| synth(Ctx, Expr, pair_type(_, ClosTypeSnd)), | |
| to_env(Ctx, Env), | |
| eval(Env, Expr, VExpr), | |
| compute_fst(VExpr, VExprFst), | |
| apply(ClosTypeSnd, VExprFst, VTypeSnd). | |
| % -------------------------------------------------------------------------------------------------- | |
| % | |
| % Parsing | |
| % | |
| % -------------------------------------------------------------------------------------------------- | |
| % TODO: Lexer is kind of broken lol | |
| % TODO: Pass name environment with EDCG | |
| whitespace --> " ". | |
| whitespace --> "\n". | |
| whitespace --> "\r". | |
| whitespace --> "\t". | |
| alpha(C) :- between(0'a, 0'z, C); between(0'A, 0'Z, C). | |
| alnum(C) :- between(0'a, 0'z, C); between(0'A, 0'Z, C); between(0'0, 0'Z, 1). | |
| name_start(C) --> [C], { alpha(C) }. | |
| name_continue(C) --> [C], { alnum(C); C =:= 0'- }. | |
| token("field") --> "field". | |
| token("let") --> "let". | |
| token("param") --> "param". | |
| token("Type") --> "Type". | |
| token("(") --> "(". | |
| token(")") --> ")". | |
| token("{") --> "}". | |
| token(";") --> ";". | |
| token(".") --> ".". | |
| token("->") --> "->". | |
| token("1") --> "1". | |
| token("2") --> "2". | |
| % token(name([Start | Rest])) --> | |
| % name_start(Start), name_continue(Rest). | |
| tokens(Tokens) --> | |
| whitespace, tokens(Tokens). | |
| tokens([Token | Tokens]) --> | |
| token(Token), tokens(Tokens). | |
| %% term(-Term:term)// is semidet. | |
| term(Term) --> | |
| term([], Term). | |
| %% term(+Names:list(string), -Term:term)// is semidet. | |
| term(Names, let(Type1, Expr1, Expr2)) --> ["let", name(Name), ":"], term(Names, Type1), ["="], | |
| term(Names, Expr1), [";"], term([Name | Names], Expr2). | |
| term(Names, Term) --> term_arrow(Names, Term). | |
| term_arrow(Names, fun_type(Type1, Type2)) --> ["param", name(Name), ":"], term(Names, Type1), [";"], | |
| term_arrow([Name | Names], Type2). | |
| term_arrow(Names, fun_type(Type1, Type2)) --> term_call(Names, Type1), ["->"], term_arrow([nil | Names], Type2). | |
| term_arrow(Names, fun_lit(Expr)) --> ["param", name(Name), ";"], term_arrow([Name | Names], Expr). | |
| term_arrow(Names, pair_type(Type1, Type2)) --> ["field", name(Name), ":"], term(Names, Type1), [";"], | |
| term_arrow([Name | Names], Type2). | |
| term_arrow(Names, pair_lit(ExprFst, ExprSnd)) --> ["field"], term(Names, ExprFst), [";"], | |
| term_arrow(Names, ExprSnd). | |
| % TODO: field x = y; x | |
| term_arrow(Names, Term) --> term_call(Names, Term). | |
| term_call(Names, fun_call(Expr1, Expr2)) --> term_call(Names, Expr1), term_atomic(Names, Expr2). | |
| term_call(Names, Term) --> term_atomic(Names, Term). | |
| term_atomic(Names, Term) --> ["("], term(Names, Term), [")"]. | |
| term_atomic(Names, var(Index)) --> [name(Name)], { lists:nth0(Index, Names, Name) }. | |
| term_atomic(_, type) --> ["Type"]. | |
| term_atomic(Names, pair_fst(Expr)) --> term_atomic(Names, Expr), [".", "1"]. | |
| term_atomic(Names, pair_snd(Expr)) --> term_atomic(Names, Expr), [".", "2"]. | |
| %% parse_term(+String:string, -Term:term) is semidet. | |
| parse_term(String, Term) :- | |
| atom_chars(String, Codes), | |
| phrase(tokens(Tokens), Codes), | |
| term(Term, Tokens, []). |
Author
Author
See also my gist on porting inference rules: https://gist.github.com/brendanzab/4cd1f7779617939e871202868b030d1c
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See this twitter thread for more info: https://twitter.com/brendanzab/status/1441216986252394503