From 63f04ae45ab5d3dd4c646b004e55922db99fbede Mon Sep 17 00:00:00 2001 From: "W. Cho" Date: Mon, 5 Feb 2024 23:04:48 +0900 Subject: [PATCH] support stdout interface and stdout write builtin function --- src/evaluator/builtin/index.ts | 20 +++++++++-- src/evaluator/index.test.ts | 63 ++++++++++++++++++++++++++-------- src/evaluator/index.ts | 9 ++++- src/evaluator/value/index.ts | 2 +- src/index.test.ts | 11 +++++- src/index.ts | 6 +++- 6 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/evaluator/builtin/index.ts b/src/evaluator/builtin/index.ts index be4eba5..36c0c19 100644 --- a/src/evaluator/builtin/index.ts +++ b/src/evaluator/builtin/index.ts @@ -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; @@ -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; } diff --git a/src/evaluator/index.test.ts b/src/evaluator/index.test.ts index 4e5eb22..2e8e497 100644 --- a/src/evaluator/index.test.ts +++ b/src/evaluator/index.test.ts @@ -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; @@ -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 = [ @@ -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", () => { diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index d4e5425..89459b1 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -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; @@ -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 { diff --git a/src/evaluator/value/index.ts b/src/evaluator/value/index.ts index 9b7aa14..a72fded 100644 --- a/src/evaluator/value/index.ts +++ b/src/evaluator/value/index.ts @@ -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 { diff --git a/src/index.test.ts b/src/index.test.ts index 3eed534..26d817d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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(["사과", "포도 바나나"]); +}); diff --git a/src/index.ts b/src/index.ts index c52c336..9a2a30d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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);