add strings
All checks were successful
Check / Explore-Gitea-Actions (push) Successful in 8s

This commit is contained in:
sfja 2026-03-17 20:24:15 +01:00
parent a42917b485
commit 4f9ea23d84
16 changed files with 287 additions and 95 deletions

View File

@ -94,6 +94,8 @@ export class Node {
return visit();
case "IntExpr":
return visit();
case "StrExpr":
return visit();
case "ArrayExpr":
return visit(...k.values);
case "IndexExpr":
@ -141,6 +143,7 @@ export type NodeKind =
| { tag: "Param"; ident: string; ty: Node | null }
| { tag: "IdentExpr"; ident: string }
| { tag: "IntExpr"; value: number }
| { tag: "StrExpr"; value: string }
| { tag: "ArrayExpr"; values: Node[] }
| { tag: "IndexExpr"; value: Node; arg: Node }
| { tag: "CallExpr"; value: Node; args: Node[] }

View File

@ -146,6 +146,10 @@ class Checker {
return Ty.Int;
}
if (node.is("StrExpr")) {
return Ty.create("Ptr", { ty: Ty.Str });
}
if (node.is("ArrayExpr")) {
let ty: Ty | null = null;
for (const value of node.kind.values) {
@ -179,6 +183,12 @@ class Checker {
) {
return Ty.create("Slice", { ty: exprTy.kind.ty });
}
if (
exprTy.is("Str") &&
argTy.compatibleWith(Ty.Int)
) {
return Ty.Int;
}
this.error(
node.loc,
`cannot use index operator on '${exprTy.pretty()}' with '${argTy.pretty()}'`,
@ -264,6 +274,8 @@ class Checker {
return Ty.Int;
case "bool":
return Ty.Bool;
case "str":
return Ty.Str;
default:
this.error(node.loc, `unknown type '${node.kind.ident}'`);
}
@ -310,9 +322,36 @@ class Checker {
private checkCall(node: ast.NodeWithKind<"CallExpr">): Ty {
if (node.kind.value.is("IdentExpr")) {
const sym = this.syms.get(node.kind.value);
if (sym && sym.tag === "Builtin") {
if (sym.id === "debug_print") {
const _argTys = node.kind.args
if (sym.tag === "Builtin") {
if (sym.id === "len") {
if (node.kind.args.length !== 1) {
this.reportArgsIncorrectAmount(
node,
node.kind.args.length,
0,
null,
);
}
const argTy = this.tys.expr(node.kind.args[0]);
if (
!(argTy.is("Array") ||
argTy.is("Ptr") &&
(argTy.kind.ty.is("Array") ||
argTy.kind.ty.is("Slice") ||
argTy.kind.ty.is("Str")))
) {
this.reportArgTypeNotCompatible(
node,
[argTy],
[Ty.Error],
null,
0,
);
}
return Ty.Int;
}
if (sym.id === "print") {
void node.kind.args
.map((arg) => this.tys.expr(arg));
return Ty.Void;
}
@ -339,11 +378,38 @@ class Checker {
.map((arg) => this.tys.expr(arg));
const params = callableTy.kind.params;
if (args.length !== params.length) {
this.reportArgsIncorrectAmount(
node,
args.length,
params.length,
calleeTy,
);
}
for (const i of args.keys()) {
if (!args[i].compatibleWith(params[i])) {
this.reportArgTypeNotCompatible(
node,
args,
params,
calleeTy,
i,
);
}
}
return callableTy.kind.retTy;
}
private reportArgsIncorrectAmount(
node: ast.NodeWithKind<"CallExpr">,
argsLength: number,
paramsLength: number,
calleeTy: Ty | null,
): never {
this.error(
node.loc,
`incorrect amount of arguments. got ${args.length} expected ${params.length}`,
`incorrect amount of arguments. got ${argsLength} expected ${paramsLength}`,
);
if (calleeTy.is("FnStmt")) {
if (calleeTy?.is("FnStmt")) {
this.info(
calleeTy.kind.stmt.loc,
"function defined here",
@ -351,15 +417,21 @@ class Checker {
}
this.fail();
}
for (const i of args.keys()) {
if (!args[i].compatibleWith(params[i])) {
private reportArgTypeNotCompatible(
node: ast.NodeWithKind<"CallExpr">,
args: Ty[],
params: Ty[],
calleeTy: Ty | null,
i: number,
): never {
this.error(
node.kind.args[i].loc,
`type '${args[i].pretty()}' not compatible with type '${
params[i].pretty()
}', for argument ${i}`,
);
if (calleeTy.is("FnStmt")) {
if (calleeTy?.is("FnStmt")) {
this.info(
calleeTy.kind.stmt.kind.params[i].loc,
`parameter '${
@ -370,9 +442,6 @@ class Checker {
}
this.fail();
}
}
return callableTy.kind.retTy;
}
private assertCompatible(left: Ty, right: Ty, loc: Loc): void {
if (!left.compatibleWith(right)) {

View File

@ -289,6 +289,10 @@ export class Parser {
const value = Number(this.current.value);
this.step();
return ast.Node.create(loc, "IntExpr", { value });
} else if (this.test("str")) {
const value = this.current.value;
this.step();
return ast.Node.create(loc, "StrExpr", { value });
} else if (this.eat("(")) {
const expr = this.parseExpr();
this.mustEat(")");
@ -414,6 +418,25 @@ export function tokenize(text: string, reporter: FileReporter): Tok[] {
.add(/0|(?:[1-9][0-9]*)/, (loc, value) => {
return { type: "int", value, loc };
})
.add(/"(?:[^\\"]|\\.)*"/, (loc, literal) => {
let i = 1;
let value = "";
while (i < literal.length - 1) {
if (literal[i] === "\\") {
i += 1;
value += {
"0": "\0",
"t": "\t",
"r": "\r",
"n": "\n",
}[literal[i]] ?? literal[i];
} else {
value += literal[i];
}
i += 1;
}
return { type: "str", value, loc };
})
.add(/./, (loc, value) => {
const escapedChar = JSON.stringify(value[0]).slice(1, -1);
reporter.error(loc, `illegal character '${escapedChar}'`);

View File

@ -113,7 +113,8 @@ class ResolverSyms {
static root(): ResolverSyms {
return new ResolverSyms(
new Map([
["debug_print", { tag: "Builtin", id: "debug_print" }],
["len", { tag: "Builtin", id: "len" }],
["print", { tag: "Builtin", id: "print" }],
]),
null,
);

View File

@ -46,7 +46,8 @@ function printMirFn(fn: mir.Fn) {
.split("\n")
.map<[string, string[]]>((line) => [line, []])
.map<[string, string[]]>(([line, colors]) => {
line = line.replace(/%(\d+)/g, "__%$1__");
line = line.replace(/%\d+/g, "__$&__");
line = line.replace(/"(?:[^"\\]|\\.)*"/g, "__$&__");
if (/^\s*__%\d+__ \(.*?\) =/.test(line)) {
line = line.replace(
/__%(\d+)__ \((.*?)\) =/g,
@ -56,15 +57,22 @@ function printMirFn(fn: mir.Fn) {
}
if (/[A-Z][a-zA-Z]+/.test(line)) {
line = line.replace(/([A-Z][a-zA-Z]+)/, "%c$1%c");
colors.push("color: red;", "");
colors.push("color: red; font-weight: bold;", "");
}
if (/\bptr\b/.test(line)) {
line = line.replace(/\b(ptr)\b/, "%c$1%c");
colors.push("color: gray;", "");
}
while (/__%\d+__/.test(line)) {
while (true) {
if (/__%\d+__/.test(line)) {
line = line.replace(/__%(\d+)__/, "%c%$1%c");
colors.push("color: lightblue;", "");
} else if (/__"(?:[^"\\]|\\.)*"__/.test(line)) {
line = line.replace(/__("(?:[^"\\]|\\.)*")__/, "%c$1%c");
colors.push("color: green;", "");
} else {
break;
}
}
return [line, colors];

View File

@ -221,9 +221,7 @@ class FnLowerer {
const valueTy = this.tys.place(value);
const arg = place.kind.arg;
const argTy = this.tys.expr(arg);
if (!valueTy.is("Array") && !valueTy.is("Slice")) {
throw new Error();
}
if (valueTy.is("Array") || valueTy.is("Slice")) {
const valueInst = this.lowerPlace(place.kind.value);
if (argTy.is("Int")) {
const argInst = this.lowerExpr(arg);
@ -249,6 +247,18 @@ class FnLowerer {
{ value: valueInst, begin, end },
);
}
}
if (valueTy.is("Str")) {
const valueInst = this.lowerPlace(place.kind.value);
if (argTy.is("Int")) {
const argInst = this.lowerExpr(arg);
return this.pushInst(
Ty.create("Ptr", { ty: Ty.Int }),
"GetElemPtr",
{ base: valueInst, offset: argInst },
);
}
}
throw new Error(
`${place.kind.tag} with arg ${argTy.pretty()} not handled`,
);
@ -282,7 +292,10 @@ class FnLowerer {
throw new Error(`'${sym.tag}' not handled`);
}
if (expr.is("IntExpr")) {
return this.pushInst(Ty.Int, "Int", { value: expr.kind.value });
return this.pushInst(ty, "Int", { value: expr.kind.value });
}
if (expr.is("StrExpr")) {
return this.pushInst(ty, "Str", { value: expr.kind.value });
}
if (expr.is("ArrayExpr")) {
const ty = this.tys.expr(expr);
@ -295,14 +308,16 @@ class FnLowerer {
return this.pushInst(ty, "Load", { source });
}
if (expr.is("CallExpr")) {
const ty = this.tys.expr(expr);
const args = expr.kind.args
.map((arg) => this.lowerExpr(arg));
if (expr.kind.value.is("IdentExpr")) {
const sym = this.syms.get(expr.kind.value);
if (sym.tag === "Builtin") {
if (sym.id === "debug_print") {
if (sym.id === "len") {
return this.pushInst(ty, "Len", { source: args[0] });
}
if (sym.id === "print") {
return this.pushInst(ty, "DebugPrint", { args });
}
throw new Error(`builtin '${sym.id}' not handled`);
@ -316,24 +331,23 @@ class FnLowerer {
return this.lowerUnaryExpr(expr);
}
if (expr.is("BinaryExpr")) {
const resultTy = this.tys.expr(expr);
const leftTy = this.tys.expr(expr.kind.left);
const rightTy = this.tys.expr(expr.kind.right);
const binaryOp = binaryOpPatterns
.find((pat) =>
expr.kind.op === pat.op &&
resultTy.compatibleWith(pat.result) &&
ty.compatibleWith(pat.result) &&
leftTy.compatibleWith(pat.left ?? pat.result) &&
rightTy.compatibleWith(pat.right ?? pat.left ?? pat.result)
);
if (!binaryOp) {
throw new Error(
`'${expr.kind.op}' with '${resultTy.pretty()}' not handled`,
`'${expr.kind.op}' with '${ty.pretty()}' not handled`,
);
}
const left = this.lowerExpr(expr.kind.left);
const right = this.lowerExpr(expr.kind.right);
return this.pushInst(resultTy, binaryOp.tag, { left, right });
return this.pushInst(ty, binaryOp.tag, { left, right });
}
throw new Error(`'${expr.kind.tag}' not handled`);
}

View File

@ -121,6 +121,8 @@ export class Inst {
case "Int":
case "Bool":
return `${k.value}`;
case "Str":
return `${JSON.stringify(k.value)}`;
case "Array":
return `[${k.values.map(r).join(", ")}]`;
case "Fn":
@ -169,6 +171,8 @@ export class Inst {
case "Div":
case "Rem":
return `${r(k.left)} ${r(k.right)}`;
case "Len":
return `${r(k.source)}`;
case "DebugPrint":
return `${k.args.map(r).join(", ")}`;
}
@ -181,6 +185,7 @@ export type InstKind =
| { tag: "Void" }
| { tag: "Int"; value: number }
| { tag: "Bool"; value: boolean }
| { tag: "Str"; value: string }
| { tag: "Array"; values: Inst[] }
| { tag: "Fn"; fn: Fn }
| { tag: "Param"; idx: number }
@ -196,6 +201,7 @@ export type InstKind =
| { tag: "Not"; source: Inst }
| { tag: "Negate"; source: Inst }
| { tag: BinaryOp; left: Inst; right: Inst }
| { tag: "Len"; source: Inst }
| { tag: "DebugPrint"; args: Inst[] };
export type BinaryOp =

View File

@ -31,6 +31,7 @@ export class FnInterpreter {
case "Void":
case "Int":
case "Bool":
case "Str":
this.regs.set(inst, new Val(k));
break;
case "Array":
@ -59,7 +60,6 @@ export class FnInterpreter {
if (base.kind.tag === "Ptr") {
const array = base.kind.value;
if (array.kind.tag !== "Array") {
console.log({ array });
throw new Error();
}
if (
@ -91,6 +91,22 @@ export class FnInterpreter {
mutable: base.kind.mutable,
}),
);
} else if (base.kind.tag === "Str") {
const str = base.kind.value;
if (
offset.kind.value < 0 ||
offset.kind.value >= str.length
) {
throw new Error();
}
this.regs.set(
inst,
new Val({
tag: "StrElemPtr",
value: str,
idx: offset.kind.value,
}),
);
} else {
throw new Error();
}
@ -172,8 +188,17 @@ export class FnInterpreter {
inst,
source.kind.values[source.kind.idx],
);
} else if (source.kind.tag === "StrElemPtr") {
this.regs.set(
inst,
new Val({
tag: "Int",
value: source.kind.value.charCodeAt(
source.kind.idx,
),
}),
);
} else {
console.log({ source });
throw new Error();
}
break;
@ -248,6 +273,23 @@ export class FnInterpreter {
case "Rem":
this.evalBinaryOp(inst, k);
break;
case "Len": {
const source = this.regs.get(k.source)!;
if (source.kind.tag === "Array") {
throw new Error();
} else if (source.kind.tag === "Str") {
this.regs.set(
inst,
new Val({
tag: "Int",
value: source.kind.value.length,
}),
);
} else {
throw new Error();
}
break;
}
case "DebugPrint":
console.log(
k.args
@ -335,8 +377,11 @@ class Val {
case "Int":
case "Bool":
return `${k.value}`;
case "Str":
return k.value;
case "Ptr":
case "ArrayElemPtr":
case "StrElemPtr":
return `<pointer>`;
case "ArraySlice":
return `[${
@ -361,6 +406,9 @@ type ValKind =
| { tag: "Void" }
| { tag: "Int"; value: number }
| { tag: "Bool"; value: boolean }
| { tag: "Str"; value: string }
| { tag: "Array"; values: Val[] }
| { tag: "Fn"; fn: mir.Fn }
| { tag: "Ptr"; mutable: boolean; value: Val }
| { tag: "ArrayElemPtr"; mutable: boolean; values: Val[]; idx: number }
| {
@ -370,5 +418,4 @@ type ValKind =
begin: number;
end: number;
}
| { tag: "Array"; values: Val[] }
| { tag: "Fn"; fn: mir.Fn };
| { tag: "StrElemPtr"; value: string; idx: number };

View File

@ -25,6 +25,7 @@ export class Ty {
static Void = Ty.create("Void", {});
static Int = Ty.create("Int", {});
static Bool = Ty.create("Bool", {});
static Str = Ty.create("Str", {});
private internHash(): string {
return JSON.stringify(this.kind);
@ -59,6 +60,9 @@ export class Ty {
if (this.is("Bool")) {
return other.is("Bool");
}
if (this.is("Str")) {
return other.is("Str");
}
if (this.is("Ptr")) {
if (!other.is("Ptr")) {
return false;
@ -123,7 +127,7 @@ export class Ty {
}
isSized(): boolean {
if (this.is("Slice")) {
if (this.is("Slice") || this.is("Str")) {
return false;
}
return true;
@ -139,6 +143,8 @@ export class Ty {
return "int";
case "Bool":
return "bool";
case "Str":
return "str";
case "Ptr":
return `*${this.kind.ty.pretty()}`;
case "PtrMut":
@ -175,6 +181,7 @@ export type TyKind =
| { tag: "Void" }
| { tag: "Int" }
| { tag: "Bool" }
| { tag: "Str" }
| { tag: "Ptr"; ty: Ty }
| { tag: "PtrMut"; ty: Ty }
| { tag: "Array"; ty: Ty; length: number }

View File

@ -5,20 +5,20 @@ fn main()
let elem: int = array[0];
// expect: 1
debug_print(elem);
print(elem);
let ptr_to_array: *[int; 3] = &array;
// expect: 2
debug_print(ptr_to_array.*[1]);
print(ptr_to_array.*[1]);
let slice: *[int] = &array[..];
// expect: 3
debug_print(slice.*[2]);
print(slice.*[2]);
let slice_mut: *mut [int] = &mut array[1..3];
slice_mut.*[0] = 4;
// expect: 4
debug_print(array[1]);
print(array[1]);
}
// vim: syntax=rust commentstring=//\ %s

View File

@ -4,6 +4,6 @@ fn main()
{
let v: int = 123;
v = 456;
debug_print(v);
print(v);
}

View File

@ -13,7 +13,7 @@ fn main()
a = 3;
}
debug_print(a);
print(a);
if false {
a = 4;
@ -21,5 +21,5 @@ fn main()
a = 5;
}
debug_print(a);
print(a);
}

View File

@ -9,7 +9,7 @@ fn main()
a = a + 1;
}
debug_print(a);
print(a);
while a < 10 {
if a >= 8 {
@ -18,5 +18,5 @@ fn main()
a = a + 1;
}
debug_print(a);
print(a);
}

View File

@ -13,20 +13,20 @@ fn main()
let a = 5;
let b = 3;
debug_print(a + b);
debug_print(a - b);
debug_print(a * b);
debug_print(a * b / 2);
debug_print(a % b);
debug_print(-a);
print(a + b);
print(a - b);
print(a * b);
print(a * b / 2);
print(a % b);
print(-a);
let c = false;
if not c {
debug_print(123);
print(123);
}
debug_print(2 * 3 + 4);
debug_print(2 * (3 + 4));
print(2 * 3 + 4);
print(2 * (3 + 4));
}

View File

@ -9,21 +9,21 @@ fn main()
let a = 1;
let b: *int = &a;
// expect: 1
debug_print(*b);
print(*b);
a = 2;
// expect: 2
debug_print(*b);
print(*b);
let c: *mut int = &mut a;
*c = 3;
// expect: 3
debug_print(a);
print(a);
// expect: 3
debug_print(*c);
print(*c);
change_to(&mut a, 4);
// expect: 4
debug_print(a);
print(a);
}

14
tests/string.ethlang Normal file
View File

@ -0,0 +1,14 @@
fn main()
{
let my_string: *str = "hello world";
// expect: hello world
print(my_string);
// expect: 104
print(my_string.*[0]);
// expect: 11
print(len(my_string));
}
// vim: syntax=rust commentstring=//\ %s