Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support stdout interface and stdout write builtin function #58

Merged
merged 1 commit into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading