Skip to content

Commit 6808f4e

Browse files
authored
Type system, classes and scoping (#1)
1 parent b91e99d commit 6808f4e

File tree

11 files changed

+658
-49
lines changed

11 files changed

+658
-49
lines changed

examples/basic.lox

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ nil; // This is nil/null
1818
// Expressions
1919
// Arithmetics
2020
var me = nil;
21-
var add = add + me;
22-
var subtract = subtract - me;
23-
var multiply = multiply * me;
24-
var divide = divide / me;
21+
var add = 23 + 41;
22+
var subtract = 13 - 4;
23+
var multiply = 13 * 4;
24+
var divide = 62 / 2;
2525

26-
var negateMe = -negateMe;
26+
var negateMe = -add;
2727

2828
// Comparison and equality
2929
var less = add < subtract;
@@ -80,36 +80,33 @@ for (var i = 1; i < 10; i = i + 1) {
8080
}
8181

8282
// Functions
83-
fun printSum(a, b) {
83+
fun printSum(a: number, b: number): void {
8484
print a + b;
8585
}
8686

87-
fun returnSum(a, b) {
87+
fun returnSum(a: number, b: number): number {
8888
return a + b;
8989
}
9090

9191
// Closures
92-
fun addPair(a, b) {
93-
return a + b;
94-
}
9592

96-
fun identity(a) {
93+
fun identity(a: (number, number) => number): (number, number) => number {
9794
return a;
9895
}
9996

100-
print identity(addPair)(1, 2); // prints "3";
97+
print identity(returnSum)(1, 2); // prints "3";
10198

102-
fun outerFunction() {
103-
fun localFunction() {
99+
fun outerFunction(): void {
100+
fun localFunction(): void {
104101
print "I'm local!";
105102
}
106103
localFunction();
107104
}
108105

109-
fun returnFunction() {
106+
fun returnFunction(): () => void {
110107
var outside = "outside";
111108

112-
fun inner() {
109+
fun inner(): void {
113110
print outside;
114111
}
115112

@@ -119,4 +116,38 @@ fun returnFunction() {
119116
var fn = returnFunction();
120117
fn();
121118

122-
// Classes are yet to come...
119+
// Classes WIP
120+
121+
class SuperClass {
122+
a: number
123+
}
124+
125+
class SubClass < SuperClass {
126+
// Nested class
127+
nested: NestedClass
128+
}
129+
130+
class NestedClass {
131+
field: string
132+
method(): string {
133+
return "execute this";
134+
}
135+
}
136+
137+
// Constructor call
138+
var x = SubClass();
139+
// Assigning nil to a class type
140+
var nilTest = SubClass();
141+
nilTest = nil;
142+
143+
// Accessing members of a class
144+
var value = x.nested.method() + "wasd";
145+
print value;
146+
147+
// Accessing members of a super class
148+
var superValue = x.a;
149+
print superValue;
150+
151+
// Assigning a subclass to a super class
152+
var superType: SuperClass = x;
153+
print superType.a;

src/language-server/lox-module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
LangiumServices, LangiumSharedServices, Module, PartialLangiumServices
44
} from 'langium';
55
import { LoxGeneratedModule, LoxGeneratedSharedModule } from './generated/module';
6+
import { LoxScopeProvider } from './lox-scope';
67
import { LoxValidationRegistry, LoxValidator } from './lox-validator';
8+
import { LoxHoverProvider } from './lsp/lox-hover-provider';
79

810
/**
911
* Declaration of custom services - add your own service classes here.
@@ -29,6 +31,12 @@ export const LoxModule: Module<LoxServices, PartialLangiumServices & LoxAddedSer
2931
validation: {
3032
ValidationRegistry: (services) => new LoxValidationRegistry(services),
3133
LoxValidator: () => new LoxValidator()
34+
},
35+
references: {
36+
ScopeProvider: (services) => new LoxScopeProvider(services)
37+
},
38+
lsp: {
39+
HoverProvider: (services) => new LoxHoverProvider(services)
3240
}
3341
};
3442

src/language-server/lox-scope.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { DefaultScopeProvider, EMPTY_SCOPE, getContainerOfType, LangiumServices, ReferenceInfo, Scope } from "langium";
2+
import { Class, isClass, MemberCall } from "./generated/ast";
3+
import { isClassType } from "./type-system/descriptions";
4+
import { getClassChain, inferType } from "./type-system/infer";
5+
6+
export class LoxScopeProvider extends DefaultScopeProvider {
7+
8+
constructor(services: LangiumServices) {
9+
super(services);
10+
}
11+
12+
override getScope(context: ReferenceInfo): Scope {
13+
// target element of member calls
14+
if (context.property === 'element') {
15+
// for now, `this` and `super` simply target the container class type
16+
if (context.reference.$refText === 'this' || context.reference.$refText === 'super') {
17+
const classItem = getContainerOfType(context.container, isClass);
18+
if (classItem) {
19+
return this.scopeClassMembers(classItem);
20+
} else {
21+
return EMPTY_SCOPE;
22+
}
23+
}
24+
const memberCall = context.container as MemberCall;
25+
const previous = memberCall.previous;
26+
if (!previous) {
27+
return super.getScope(context);
28+
}
29+
const previousType = inferType(previous, new Map());
30+
if (isClassType(previousType)) {
31+
return this.scopeClassMembers(previousType.literal);
32+
}
33+
return EMPTY_SCOPE;
34+
}
35+
return super.getScope(context);
36+
}
37+
38+
private scopeClassMembers(classItem: Class): Scope {
39+
const allMembers = getClassChain(classItem).flatMap(e => e.members);
40+
return this.createScopeForNodes(allMembers);
41+
}
42+
}
Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { ValidationChecks, ValidationRegistry } from 'langium';
2-
import { LoxAstType } from './generated/ast';
1+
import { AstNode, streamAllContents, ValidationAcceptor, ValidationChecks, ValidationRegistry } from 'langium';
2+
import { BinaryExpression, ExpressionBlock, FunctionDeclaration, isReturnStatement, LoxAstType, MethodMember, TypeReference, UnaryExpression, VariableDeclaration } from './generated/ast';
33
import type { LoxServices } from './lox-module';
4+
import { isAssignable } from './type-system/assignment';
5+
import { isVoidType, TypeDescription, typeToString } from './type-system/descriptions';
6+
import { inferType } from './type-system/infer';
7+
import { isLegalOperation } from './type-system/operator';
48

59
/**
610
* Registry for validation checks.
@@ -10,6 +14,11 @@ export class LoxValidationRegistry extends ValidationRegistry {
1014
super(services);
1115
const validator = services.validation.LoxValidator;
1216
const checks: ValidationChecks<LoxAstType> = {
17+
BinaryExpression: validator.checkBinaryOperationAllowed,
18+
UnaryExpression: validator.checkUnaryOperationAllowed,
19+
VariableDeclaration: validator.checkVariableDeclaration,
20+
MethodMember: validator.checkMethodReturnType,
21+
FunctionDeclaration: validator.checkFunctionReturnType
1322
};
1423
this.register(checks, validator);
1524
}
@@ -20,13 +29,89 @@ export class LoxValidationRegistry extends ValidationRegistry {
2029
*/
2130
export class LoxValidator {
2231

23-
// checkPersonStartsWithCapital(person: Person, accept: ValidationAcceptor): void {
24-
// if (person.name) {
25-
// const firstChar = person.name.substring(0, 1);
26-
// if (firstChar.toUpperCase() !== firstChar) {
27-
// accept('warning', 'Person name should start with a capital.', { node: person, property: 'name' });
28-
// }
29-
// }
30-
// }
32+
checkFunctionReturnType(func: FunctionDeclaration, accept: ValidationAcceptor): void {
33+
this.checkFunctionReturnTypeInternal(func.body, func.returnType, accept);
34+
}
35+
36+
checkMethodReturnType(method: MethodMember, accept: ValidationAcceptor): void {
37+
this.checkFunctionReturnTypeInternal(method.body, method.returnType, accept);
38+
}
39+
40+
private checkFunctionReturnTypeInternal(body: ExpressionBlock, returnType: TypeReference, accept: ValidationAcceptor): void {
41+
const map = this.getTypeCache();
42+
const returnStatements = streamAllContents(body).filter(isReturnStatement).toArray();
43+
const expectedType = inferType(returnType, map);
44+
if (returnStatements.length === 0 && !isVoidType(expectedType)) {
45+
accept('error', "A function whose declared type is not 'void' must return a value.", {
46+
node: returnType
47+
});
48+
return;
49+
}
50+
for (const returnStatement of returnStatements) {
51+
const returnValueType = inferType(returnStatement, map);
52+
if (!isAssignable(returnValueType, expectedType)) {
53+
accept('error', `Type '${typeToString(returnValueType)}' is not assignable to type '${typeToString(expectedType)}'.`, {
54+
node: returnStatement
55+
});
56+
}
57+
}
58+
}
59+
60+
checkVariableDeclaration(decl: VariableDeclaration, accept: ValidationAcceptor): void {
61+
if (decl.type && decl.value) {
62+
const map = this.getTypeCache();
63+
const left = inferType(decl.type, map);
64+
const right = inferType(decl.value, map);
65+
if (!isAssignable(right, left)) {
66+
accept('error', `Type '${typeToString(right)}' is not assignable to type '${typeToString(left)}'.`, {
67+
node: decl,
68+
property: 'value'
69+
});
70+
}
71+
} else if (!decl.type && !decl.value) {
72+
accept('error', 'Variables require a type hint or an assignment at creation', {
73+
node: decl,
74+
property: 'name'
75+
});
76+
}
77+
}
78+
79+
checkBinaryOperationAllowed(binary: BinaryExpression, accept: ValidationAcceptor): void {
80+
const map = this.getTypeCache();
81+
const left = inferType(binary.left, map);
82+
const right = inferType(binary.right, map);
83+
if (!isLegalOperation(binary.operator, left, right)) {
84+
accept('error', `Cannot perform operation '${binary.operator}' on values of type '${typeToString(left)}' and '${typeToString(right)}'.`, {
85+
node: binary
86+
})
87+
} else if (binary.operator === '=') {
88+
if (!isAssignable(right, left)) {
89+
accept('error', `Type '${typeToString(right)}' is not assignable to type '${typeToString(left)}'.`, {
90+
node: binary,
91+
property: 'right'
92+
})
93+
}
94+
} else if (['==', '!='].includes(binary.operator)) {
95+
if (!isAssignable(right, left)) {
96+
accept('warning', `This comparison will always return '${binary.operator === '==' ? 'false' : 'true'}' as types '${typeToString(left)}' and '${typeToString(right)}' are not compatible.`, {
97+
node: binary,
98+
property: 'operator'
99+
});
100+
}
101+
}
102+
}
103+
104+
checkUnaryOperationAllowed(unary: UnaryExpression, accept: ValidationAcceptor): void {
105+
const item = inferType(unary.value, this.getTypeCache());
106+
if (!isLegalOperation(unary.operator, item)) {
107+
accept('error', `Cannot perform operation '${unary.operator}' on value of type '${typeToString(item)}'.`, {
108+
node: unary
109+
});
110+
}
111+
}
112+
113+
private getTypeCache(): Map<AstNode, TypeDescription> {
114+
return new Map();
115+
}
31116

32117
}

0 commit comments

Comments
 (0)