Skip to content

Commit

Permalink
support stdout interface and stdout write builtin function (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
wcho21 authored Feb 5, 2024
1 parent 57c5837 commit 68a1959
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 20 deletions.
20 changes: 18 additions & 2 deletions src/evaluator/builtin/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type * as Value from "../value";
import * as value from "../value";

export type BuiltinFunction = (args: any) => Value.Value;
export type BuiltinFunction = (args: any[], onStdout?: (toWrite: string) => void) => Value.Value;

const len: BuiltinFunction = (args: Value.Value[]) => {
const len: BuiltinFunction = (args) => {
const arg = args[0];
if (arg.type === "string") {
const length = arg.value.length;
Expand All @@ -13,11 +13,27 @@ const len: BuiltinFunction = (args: Value.Value[]) => {
throw new Error();
};

const write: BuiltinFunction = (args, onStdout) => {
if (args.length === 0) {
throw new Error();
}

const str = args.map(arg => arg.representation).join(" ");
if (onStdout !== undefined) {
onStdout(str);
}

const range = { begin: args[0].range.begin, end: args[args.length-1].range.end };
return value.createEmptyValue({ value: null }, "(없음)", range);
};

const builtins = {
get(identifier: string): BuiltinFunction | null {
switch (identifier) {
case "길이":
return len;
case "쓰기":
return write;
default:
return null;
}
Expand Down
63 changes: 49 additions & 14 deletions src/evaluator/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import Parser from "../parser";
import Evaluator, * as Eval from "./";
import Environment from "./environment";

const evaluateInput = (input: string) => {
const evaluateInput = (input: string, onStdout?: (toWrite: string) => void) => {
const lexer = new Lexer(input);
const parser = new Parser(lexer);
const parsed = parser.parseSource();

const evaluator = new Evaluator();
if (onStdout !== undefined) {
evaluator.onStdout(onStdout);
}
const env = new Environment();
const evaluated = evaluator.evaluate(parsed, env);
return evaluated;
Expand All @@ -35,6 +38,14 @@ const testEvaluatingFunction = ({ input, expectedParamsLength }: { input: string
expect(evaluated).toHaveProperty("environment");
};

const testEvaluatingStdout = ({ input, expected }: { input: string, expected: any }): void => {
const stdouts: string[] = [];

evaluateInput(input, toWrite => stdouts.push(toWrite));

expect(stdouts).toEqual(expected);
};

describe("evaluate()", () => {
describe("single numbers", () => {
const cases = [
Expand Down Expand Up @@ -358,20 +369,44 @@ describe("evaluate()", () => {
});

describe("builtin function calls", () => {
const cases = [
{
name: "length function for empty string",
input: "길이('')",
expected: 0,
},
{
name: "length function for nonempty string",
input: "길이('사과')",
expected: 2,
},
];
describe("길이()", () => {
const cases = [
{
name: "empty string",
input: "길이('')",
expected: 0,
},
{
name: "nonempty string",
input: "길이('사과')",
expected: 2,
},
];

it.each(cases)("evaluate $name", testEvaluatingPrimitive);
});

it.each(cases)("evaluate $name", testEvaluatingPrimitive);
describe("쓰기()", () => {
const cases = [
{
name: "single string",
input: "쓰기('사과')",
expected: ["사과"],
},
{
name: "multiple string",
input: "쓰기('사과', '포도', '바나나')",
expected: ["사과 포도 바나나"],
},
{
name: "multiple calls",
input: "쓰기('사과') 쓰기('포도')",
expected: ["사과", "포도"],
},
];

it.each(cases)("evaluate $name", testEvaluatingStdout);
});
});

describe("errors", () => {
Expand Down
9 changes: 8 additions & 1 deletion src/evaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ export class BadIdentifierError extends EvalError {};
type ComparisonOperator = "==" | "!=" | ">" | "<" | ">=" | "<=";

export default class Evaluator {
private callbackOnStdout?: (toWrite: string) => void;

evaluate(node: Node.ProgramNode, env: Environment): Value.Value {
return this.evaluateProgram(node, env);
}

onStdout(callback: (toWrite: string) => void): void {
this.callbackOnStdout = callback;
}

private evaluateProgram(node: Node.ProgramNode, env: Environment): Value.Value {
const { statements } = node;

Expand Down Expand Up @@ -242,7 +248,8 @@ export default class Evaluator {
}

private evaluateBuiltinFunctionCall(func: Value.BuiltinFunctionValue, callArguments: Value.Value[]): Value.Value {
return func.body(callArguments);
const callbackOnStdout = this.callbackOnStdout === undefined ? undefined : this.callbackOnStdout.bind(this);
return func.body(callArguments, callbackOnStdout);
}

private getBooleanComparisonInfixOperationValue(left: boolean, right: boolean, operator: ComparisonOperator): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/evaluator/value/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface EmptyValue extends ValueBase<"empty"> {
readonly value: null,
}
export interface BuiltinFunctionValue extends ValueBase<"builtin function"> {
readonly body: (args: any) => Value,
readonly body: (args: any, onStdout?: (toWrite: string) => void) => Value,
}

export interface ReturnValue {
Expand Down
11 changes: 10 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ it("execute assignment and calculation", () => {
expect(execute("변수1 = 4 변수2 = 9 ((변수1 - 변수2) * 변수1)")).toBe("-20");
});

it("execute builtin 길이 function", () => {
it("execute builtin function 길이()", () => {
expect(execute("길이('사과')")).toBe("2");
});

it("execute builtin function 쓰기()", () => {
const stdouts: string[] = [];
const onStdout = (toWrite: string) => stdouts.push(toWrite);

execute("쓰기('사과') 쓰기('포도', '바나나')", onStdout);

expect(stdouts).toEqual(["사과", "포도 바나나"]);
});
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import Lexer from "./lexer";
import Parser from "./parser";
import Evaluator, { Environment } from "./evaluator";

export const execute = (input: string): string => {
export const execute = (input: string, callbackOnStdout?: (toWrite: string) => void): string => {
const lexer = new Lexer(input);
const parser = new Parser(lexer);
const parsed = parser.parseSource();

const evaluator = new Evaluator();
if (callbackOnStdout !== undefined) {
evaluator.onStdout(callbackOnStdout);
}

const environment = new Environment();
const evaluated = evaluator.evaluate(parsed, environment);

Expand Down

0 comments on commit 68a1959

Please sign in to comment.