Add to chapter 8

This commit is contained in:
sfja 2024-11-04 01:46:22 +01:00
parent 735e7e5974
commit 7af55dc9b9

View File

@ -3,6 +3,8 @@
We'd like to be able to compile to a relatively low level. To this end, we need to have predetermined the types of the values used in the program code.
We'll use 2 different concepts of types. One is parsed types. There are the ones the programmer writes explicitly. These we'll callexplicit types or `EType`. The other is calcaluted types, ie. result of type checking. These types we'll call value types or `VType`, because they tell us, within the compiler, the types of values.
## 8.1 Explicit types
For a type checker to be able to determine all the types of all values, the program is required to contain the information need. That information will be dictated by the programmer, therefore the programmer needs a way to write types explicitly. We'll do this by adding types to the language.
@ -27,15 +29,184 @@ let op: fn(int, int) -> int = add;
let values: [int] = array();
```
### 8.1.2 Parsed explicit types
To parse explicit types, we'll need to add them to the AST definitions.
### 8.1.2 Parsing parameters
```ts
type EType = {
kind: ETypeKind,
pos: Pos,
id: number,
}
type ETypeKind =
| { type: "error" }
| { type: "ident", value: string }
| { type: "array", inner: EType }
;
```
```ts
class Parser {
// ...
private etype(kind: ETypeKind, pos: Pos): EType {
const id = this.nextNodeId;
this.nextNodeId += 1;
return { kind, pos, id };
}
// ...
}
```
Just like expressions and statements, explicit types consists of multiple forms. We also want ids, like on statements and expressions.
```ts
class Parser {
// ...
public parseEType(): EType {
const pos = this.pos();
if (this.test("ident")) {
const ident = this.current().identValue!;
return this.etype({ type: "ident", value: ident });
}
if (this.test("[")) {
this.step();
const inner = this.parseEType();
if (!this.test("]")) {
this.report("expected ']'", pos);
return this.etype({ type: "error" }, pos);
}
this.step();
return this.etype({ type: "array", inner });
}
// ...
this.report("expected type");
return this.etype({ type: "error" }, pos);
}
// ...
}
```
We currently have implemented 2 types of explicit types: identifier types and array types. Identifier types are just like identifier expressions, ie. an identifier token. An array type is a type enclosed in `[` and `]`, eg. `[int]`.
### 8.1.3 Parsing parameters
Both function definitions and let-statements use parameters.
### 8.1.3 Parsing functions
We'll therefore add types to parameters.
## 8.2 Types in AST
```ts
type Param = {
ident: string,
etype?: EType,
pos: Pos,
};
```
We'll add the `etype` field. The field is optional, because we'll do type inference.
```ts
class Parser {
// ...
public parseParam(): { ok: true, value: Param } | { ok: false } {
const pos = this.pos();
if (this.test("ident")) {
const ident = this.current().identValue!;
this.step();
if (this.test(":")) {
const etype = this.parseEType();
return { ok: true, value: { ident, etype, pos } };
}
return { ok: true, value: { ident, pos } };
}
this.report("expected param");
return { ok: false };
}
// ...
}
```
We've added the if-statement checking for a `:`-token.
### 8.1.4 Parsing functions
```ts
type StmtKind =
// ...
| {
type: "fn",
ident: string,
params: Param[],
returnType?: EType,
body: Expr,
}
// ...
;
```
We've added the `returnType` field. It is optional, because the return type my be omitted, in which case, it will be assigned null as the return type.
```ts
class Parser {
// ...
public parseFn(): Stmt {
const pos = this.pos();
this.step();
if (!this.test("ident")) {
this.report("expected ident");
return this.stmt({ type: "error" }, pos);
}
const ident = this.current().identValue!;
this.step();
if (!this.test("(")) {
this.report("expected '('");
return this.stmt({ type: "error" }, pos);
}
const params = this.parseFnParams();
let returnType: EType | null = null;
if (this.test("->")) {
this.step();
returnType = this.parseEType();
}
if (!this.test("{")) {
this.report("expected block");
return this.stmt({ type: "error" }, pos);
}
const body = this.parseBlock();
if (returnType === null) {
return this.stmt({ type: "fn", ident, params, body }, pos);
}
return this.stmt({ type: "fn", ident, params, returnType, body }, pos);
}
// ...
}
```
We've added the if statement checking for `->`, and the handling of the mutable `returnType` variable.
## 8.2 Value types
The type checking process takes the program with some amount of explicit types. It then figures out the types of all values and operations. In this process, it will also check that all values and operations operate on and have themselves valid and appropriate value types. The type checker works with and produces value types or `VType`.
```ts
type VType =
| { type: "error" }
| { type: "unkown" }
| { type: "null" }
| { type: "int" }
| { type: "string" }
| { type: "bool" }
| { type: "array", inner: VType }
| { type: "struct" }
| { type: "fn", params: VType[], returnType: VType }
```
The error-type is for convenience of implementation, just like in the parser. The unknown-type is for type inference, when we don't yet know the type. the null-, int-, string-, bool- and struct-types are uniform in that all values of each type can be assigned to each type. There's no difference between to integers, although the values might differ, and the same for two struct values, even though they may contain a different set of fields. Array-types and function-types also contain their details, ie. inner type in arrays and params and return type in functions.
### 8.2.1 Value types in AST
When checking the types, we need to store the resulting value types. We'll choose to store these inside the AST itself. This means we'll mutate the AST in the type checker.
```ts
type Expr = {
@ -50,3 +221,104 @@ type Stmt = {
}
```
## 8.3 The checker class
```ts
class Checker {
// ...
}
```
The checker class will by similar to the resolver class, from an architectural perspective.
### 8.3.1 Reporting errors
```ts
class Checker {
// ...
public report(msg: string, pos: Pos) {
console.error(`${msg} at ${pos.line}:${pos.col}`);
}
// ...
}
```
## 8.4 Checking explicit types
```ts
class Checker {
// ...
public checkEType(etype: EType): VType {
const pos = etype.pos;
// ...
throw new Error(`unknown explicit type ${etype.type}`);
}
// ...
}
```
### 8.4.1 Checking explicit identifier types
```ts
class Checker {
// ...
public checkEType(etype: EType): VType {
// ...
if (etype.type === "ident") {
if (etype.value === "null")
return { type: "null" };
if (etype.value === "int")
return { type: "int" };
if (etype.value === "bool")
return { type: "bool" };
if (etype.value === "string")
return { type: "string" };
if (etype.value === "struct")
return { type: "struct" };
this.report(`undefined type '${etype.value}'`);
return { type: "error" };
}
// ...
}
// ...
}
```
We test if the identifier value matches the known types.
### 8.4.2 Checking explicit array types
```ts
class Checker {
// ...
public checkEType(etype: EType): VType {
// ...
if (etype.type === "array") {
const inner = this.checkEType(etype.inner);
return { type: "array", inner };
}
// ...
}
// ...
}
```
We check the inner type recursively, then return an array value type.
## 8.5 Checking expressions
```ts
class Checker {
// ...
public checkExpr(expr: Expr): VType {
const pos = expr.pos;
// ...
throw new Error(`unknown expression ${expr.type}`);
}
// ...
}
```
**To be finished.**