Skip to content

Commit

Permalink
feat: support c-style for loop
Browse files Browse the repository at this point in the history
  • Loading branch information
HenryZhang-ZHY committed Nov 14, 2023
1 parent 06355ad commit 0b3702a
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 22 deletions.
43 changes: 32 additions & 11 deletions docs/.vitepress/components/Playground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import {F_Object, runWithPrint} from '@funkey/interpreter'
import Editor from './Editor.vue'
import {onMounted, ref} from 'vue'
const code = ref<string>(`let add = fn(a, b) { a + b; };
let result = add(512, 2048);
print(result);`)
const code = ref<string>(`for (let i = 1; i < 10; i = i + 1) {
for (let j = 1; j <= i; j = j + 1) {
print(j);
print("*");
print(i);
print("=");
print(j*i);
print(" ");
}
print("\\n");
}`)
const onCodeUpdate = (e) => {
code.value = e
Expand All @@ -16,12 +22,24 @@ const onCodeUpdate = (e) => {
const output = ref<HTMLDivElement | null>(null)
const run = () => {
const lines: string[] = []
const buffer: string[] = []
const print = (...args: F_Object[]) => {
lines.push(args.map(x => x.inspect).join(' '))
buffer.push(args.map(x => x.inspect).join(' '))
}
try {
runWithPrint(code.value, print)
} catch (error) {
let currentError = error
let space = ''
while (currentError) {
buffer.push(`<span style="color: red">${space}${currentError.message}</span>`)
space += '&nbsp;&nbsp;'
currentError = currentError.innerError
}
} finally {
output.value.innerHTML = buffer.join('').replaceAll('\\n', '<br/>')
}
runWithPrint(code.value, print)
output.value.innerHTML = lines.join('<br/>')
}
onMounted(() => {
Expand All @@ -43,8 +61,9 @@ onMounted(() => {

<style scoped>
#playground {
max-width: calc(var(--vp-layout-max-width) - 64px);
height: calc(80vh - var(--vp-nav-height));
min-height: 400px;
max-width: calc(var(--vp-layout-max-width) - 64px);
margin: 8px auto;
}
Expand All @@ -69,7 +88,7 @@ onMounted(() => {
#output {
width: 100%;
height: 28%;
min-height: 40%;
margin: 16px 0;
padding: 16px;
Expand All @@ -78,5 +97,7 @@ onMounted(() => {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
font-family: Consolas, serif;
overflow: auto;
}
</style>
56 changes: 56 additions & 0 deletions packages/interpreter/src/ast/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ export interface AstVisitor {
visitBlockStatement: (x: BlockStatement) => void
visitLetStatement: (x: LetStatement) => void
visitReturnStatement: (x: ReturnStatement) => void
visitForStatement: (x: ForStatement) => void
visitPrefixExpression: (x: PrefixExpression) => void
visitInfixExpression: (x: InfixExpression) => void
visitCallExpression: (x: CallExpression) => void
visitIndexExpression: (x: IndexExpression) => void
visitDotExpression: (x: DotExpression) => void
visitAssignExpression: (x: AssignExpression) => void
visitIntegerLiteral: (x: IntegerLiteral) => void
visitBooleanLiteral: (x: BooleanLiteral) => void
visitStringLiteral: (x: StringLiteral) => void
Expand Down Expand Up @@ -306,6 +308,31 @@ export class DotExpression implements Expression {
}
}

export class AssignExpression implements Expression {
private readonly token: Token

readonly left: Expression
readonly right: Expression

constructor(token: Token, left: Expression, right: Expression) {
this.token = token
this.left = left
this.right = right
}

get tokenLiteral(): string {
return this.token.literal
}

toString(): string {
return `(${this.left}.${this.right})`
}

accept(visitor: AstVisitor): void {
visitor.visitAssignExpression(this)
}
}

export class PrefixExpression implements Expression {
private readonly token: Token

Expand Down Expand Up @@ -446,6 +473,35 @@ export class LetStatement implements Statement {
}
}

export class ForStatement implements Statement {
private readonly token: Token

readonly bodyStatement: BlockStatement
readonly initializationStatement: Statement | undefined
readonly conditionStatement: ExpressionStatement | undefined
readonly updateStatement: Statement | undefined

constructor(token: Token, bodyStatement: BlockStatement, initializationStatement: Statement | undefined = undefined, conditionStatement: ExpressionStatement | undefined = undefined, updateStatement: Statement | undefined = undefined) {
this.token = token
this.bodyStatement = bodyStatement
this.initializationStatement = initializationStatement
this.conditionStatement = conditionStatement
this.updateStatement = updateStatement
}

get tokenLiteral(): string {
return this.token.literal
}

toString(): string {
return `for(${this.initializationStatement};${this.conditionStatement};${this.updateStatement}){${this.bodyStatement}}`
}

accept(visitor: AstVisitor): void {
visitor.visitForStatement(this)
}
}

export class ReturnStatement implements Statement {
private readonly token: Token

Expand Down
28 changes: 28 additions & 0 deletions packages/interpreter/src/evaluator/evaluator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ describe('evaluate InFixExpression', () => {
{input: '1 > 2', result: false},
{input: '1 < 1', result: false},
{input: '1 > 1', result: false},
{input: '1 >= 1', result: true},
{input: '2 >= 1', result: true},
{input: '1 <= 1', result: true},
{input: '1 <= 2', result: true},
{input: '1 == 1', result: true},
{input: '1 != 1', result: false},
{input: '1 == 2', result: false},
Expand All @@ -128,6 +132,7 @@ describe('evaluate InFixExpression', () => {
{input: '-5', result: -5},
{input: '-10', result: -10},
{input: '5 + 5 + 5 + 5 - 10', result: 10},
{input: '11 % 10', result: 1},
{input: '2 * 2 * 2 * 2 * 2', result: 32},
{input: '-50 + 100 + -50', result: 0},
{input: '5 * 2 + 10', result: 20},
Expand Down Expand Up @@ -186,6 +191,17 @@ describe('evaluate DotExpression', () => {
})
})

describe('evaluate DotExpression', () => {
test.each([
{input: 'let a = 1; a = 2; a', result: 2,},
])('evaluate [$input] should get [$result]', ({input, result}) => {
const evaluated = evaluate(input)

assert(evaluated instanceof F_Integer)
expect(evaluated.value).toBe(result)
})
})

describe('evaluate IfExpression', () => {
test.each([
{input: 'if (true) { 10 }', result: 10},
Expand Down Expand Up @@ -239,6 +255,18 @@ describe('evaluate ReturnStatement', () => {
})
})

describe('evaluate ForStatement', () => {
test.each([
{input: 'let r = 0; for (let i = 0; i < 10; i = i + 1) {r = r + 1;} r', result: 10,},
{input: 'let r = 0; for (let i = 0; i < 10; i = i + 1) { for (let j = 0; j < 10; j = j + 1) {r = r + 1;}} r', result: 100,},
])('evaluate [$input] should get [$result]', ({input, result}) => {
const evaluated = evaluate(input)

assert(evaluated instanceof F_Integer)
expect(evaluated.value).toBe(result)
})
})

describe('error handling', () => {
test.each([
{input: '5 + true;', expectedErrorMessage: 'type mismatch: Integer + Boolean'},
Expand Down
46 changes: 42 additions & 4 deletions packages/interpreter/src/evaluator/evaluator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
ArrayLiteral,
ArrayLiteral, AssignExpression,
AstVisitor, BlockStatement,
BooleanLiteral, CallExpression, DotExpression,
ExpressionStatement, FunctionLiteral, Identifier, IfExpression, IndexExpression, InfixExpression,
ExpressionStatement, ForStatement, FunctionLiteral, Identifier, IfExpression, IndexExpression, InfixExpression,
IntegerLiteral, LetStatement, MapLiteral,
Node,
PrefixExpression,
Expand Down Expand Up @@ -169,6 +169,21 @@ class AstEvaluatorVisitor implements AstVisitor {
}
}

visitForStatement(x: ForStatement) {
const environment = new Environment(this.environment)
if (x.initializationStatement) {
evaluate(x.initializationStatement, environment)
}

while (x.conditionStatement === undefined || this.isTruthy(evaluate(x.conditionStatement, environment))) {
evaluate(x.bodyStatement, environment)
if (x.updateStatement) {
evaluate(x.updateStatement, environment)

}
}
}

visitPrefixExpression(x: PrefixExpression) {
switch (x.operator) {
case '!':
Expand All @@ -195,13 +210,19 @@ class AstEvaluatorVisitor implements AstVisitor {
}

visitInfixExpression(x: InfixExpression) {
if (x.operator === '=' && x.left instanceof Identifier) {
this.environment.setVariableValue(x.left.value, evaluate(x.right, this.environment))
return
}

const left = evaluate(x.left, this.environment)
const right = evaluate(x.right, this.environment)
switch (x.operator) {
case '+':
case '-':
case '*':
case '/':
case '%':
if (left instanceof F_Integer && right instanceof F_Integer) {
this._result = evaluateArithmeticExpression(left, x.operator, right)
break
Expand All @@ -216,7 +237,9 @@ class AstEvaluatorVisitor implements AstVisitor {
}
}
case '>':
case '>=':
case '<':
case '<=':
assert(left instanceof F_Integer)
assert(right instanceof F_Integer)
this._result = evaluateComparingExpression(left, x.operator, right)
Expand All @@ -229,7 +252,7 @@ class AstEvaluatorVisitor implements AstVisitor {
break
}

function evaluateArithmeticExpression(left: F_Integer, operator: '+' | '-' | '*' | '/', right: F_Integer): F_Integer {
function evaluateArithmeticExpression(left: F_Integer, operator: '+' | '-' | '*' | '/' | '%', right: F_Integer): F_Integer {
switch (operator) {
case '+':
return packNativeValue(left.value + right.value)
Expand All @@ -239,10 +262,12 @@ class AstEvaluatorVisitor implements AstVisitor {
return packNativeValue(left.value * right.value)
case '/':
return packNativeValue(left.value / right.value)
case '%':
return packNativeValue(left.value % right.value)
}
}

function evaluateComparingExpression(left: F_Integer, operator: '>' | '<' | '==' | '!=', right: F_Integer): F_Boolean {
function evaluateComparingExpression(left: F_Integer, operator: '>' | '<' | '==' | '!=' | '>=' | '<=', right: F_Integer): F_Boolean {
switch (operator) {
case '>':
return packNativeValue(left.value > right.value)
Expand All @@ -252,6 +277,10 @@ class AstEvaluatorVisitor implements AstVisitor {
return packNativeValue(left.value == right.value)
case '!=':
return packNativeValue(left.value != right.value)
case '>=':
return packNativeValue(left.value >= right.value)
case '<=':
return packNativeValue(left.value <= right.value)
}
}

Expand Down Expand Up @@ -306,6 +335,15 @@ class AstEvaluatorVisitor implements AstVisitor {
this._result = result
}

visitAssignExpression(x: AssignExpression) {
const {left, right} = x
if (!(left instanceof Identifier)) {
throw new Error('left side of assignment should be a identifier')
}

this.environment.setVariableValue(left.value, evaluate(right, this.environment))
}

visitIndexExpression(x: IndexExpression) {
const left = evaluate(x.left, this.environment)
const index = evaluate(x.index, this.environment)
Expand Down
6 changes: 5 additions & 1 deletion packages/interpreter/src/lexer/lexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {TokenType} from '../token/tokenType'

describe('nextToken', () => {
test.each([
{input: 'let', expectedToken: new Token(TokenType.LET, 'let', 0, 0)}
{input: 'let', expectedToken: new Token(TokenType.LET, 'let', 0, 0)},
{input: 'for', expectedToken: new Token(TokenType.FOR, 'for', 0, 0)},
{input: '%', expectedToken: new Token(TokenType.MOD, '%', 0, 0)},
{input: '>=', expectedToken: new Token(TokenType.GTE, '>=', 0, 0)},
{input: '<=', expectedToken: new Token(TokenType.LTE, '<=', 0, 0)},
])(
'Token should be $expectedToken.type, $expectedToken.literal',
({input, expectedToken}) => {
Expand Down
19 changes: 17 additions & 2 deletions packages/interpreter/src/lexer/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,22 @@ export class Lexer {
}
break
case '<':
token = this.generateNoLiteralToken(TokenType.LT)
if (this.nextChar === '=') {
token = this.generateNoLiteralToken(TokenType.LTE)

this.next()
} else {
token = this.generateNoLiteralToken(TokenType.LT)
}
break
case '>':
token = this.generateNoLiteralToken(TokenType.GT)
if (this.nextChar === '=') {
token = this.generateNoLiteralToken(TokenType.GTE)

this.next()
} else {
token = this.generateNoLiteralToken(TokenType.GT)
}
break
case '+':
token = this.generateNoLiteralToken(TokenType.PLUS)
Expand All @@ -90,6 +102,9 @@ export class Lexer {
case '/':
token = this.generateNoLiteralToken(TokenType.SLASH)
break
case '%':
token = this.generateNoLiteralToken(TokenType.MOD)
break
case '.':
token = this.generateNoLiteralToken(TokenType.DOT)
break
Expand Down
Loading

0 comments on commit 0b3702a

Please sign in to comment.