Skip to content

Commit 46317e3

Browse files
Kyriel Abadmartin-henz
Kyriel Abad
andauthoredMar 21, 2024··
Update parser to new scm-slang parser (#1584)
* Prepare scheme files for new parser * update JS version for js-slang * proper formatting of files * fix separate program environments across REPL eval calls * remove logger messages from interpreter * Enable variadic continuations for future * Remove Infinity and NaN representation from Scheme * Change scm-slang to follow forked version * update scm-slang to newest parser * resolve linting problems * add test cases to verify proper chapter validation, decoded representation * update scm-slang * Move scheme-specific tests to scm-slang * make scheme test names more obvious * Revert "Move scheme-specific tests to scm-slang" This reverts commit 42e184e. * move scm-slang to dedicated alt-lang folder * remove duplicate code between scm-slang and js-slang * ignore alt langs coverage --------- Co-authored-by: Martin Henz <[email protected]>
1 parent 89b726c commit 46317e3

21 files changed

+387
-2931
lines changed
 

‎.eslintignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
/dist/
2-
/src/scm-slang/
2+
/src/alt-langs/
33
/src/py-slang/

‎.gitmodules

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
[submodule "src/scm-slang"]
2-
path = src/scm-slang
3-
url = https://github.com/source-academy/scm-slang.git
1+
[submodule "src/alt-langs/scheme/scm-slang"]
2+
path = src/alt-langs/scheme/scm-slang
3+
url = https://github.com/source-academy/scm-slang
44
[submodule "src/py-slang"]
55
path = src/py-slang
66
url = https://github.com/source-academy/py-slang

‎.prettierignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
/src/scm-slang/
1+
/src/alt-langs/
22
/src/py-slang/
33
/src/**/__tests__/**/__snapshots__

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
"/node_modules/",
115115
"/src/typings/",
116116
"/src/utils/testing.ts",
117-
"/src/scm-slang",
117+
"/src/alt-langs",
118118
"/src/py-slang/"
119119
],
120120
"reporters": [

‎src/__tests__/__snapshots__/scmlib.ts.snap

-1,535
This file was deleted.

‎src/__tests__/scmlib.ts

-1,186
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Node } from 'estree'
2+
3+
import { UnassignedVariable } from '../../../errors/errors'
4+
import { decode, encode } from '../scm-slang/src'
5+
import { cons, set$45$cdr$33$ } from '../scm-slang/src/stdlib/base'
6+
import { dummyExpression } from '../../../utils/dummyAstCreator'
7+
import { decodeError, decodeValue } from '../../../parser/scheme'
8+
9+
describe('Scheme encoder and decoder', () => {
10+
it('encoder and decoder are proper inverses of one another', () => {
11+
const values = [
12+
'hello',
13+
'hello world',
14+
'scheme->JavaScript',
15+
'hello world!!',
16+
'true',
17+
'false',
18+
'null',
19+
'1',
20+
'😀'
21+
]
22+
for (const value of values) {
23+
expect(decode(encode(value))).toEqual(value)
24+
}
25+
})
26+
27+
it('decoder ignores primitives', () => {
28+
const values = [1, 2, 3, true, false, null]
29+
for (const value of values) {
30+
expect(decodeValue(value)).toEqual(value)
31+
}
32+
})
33+
34+
it('decoder works on function toString representation', () => {
35+
// Dummy function to test
36+
function $65$() {}
37+
expect(decodeValue($65$).toString()).toEqual(`function A() { }`)
38+
})
39+
40+
it('decoder works on circular lists', () => {
41+
function $65$() {}
42+
const pair = cons($65$, 'placeholder')
43+
set$45$cdr$33$(pair, pair)
44+
expect(decodeValue(pair).toString()).toEqual(`function A() { },`)
45+
})
46+
47+
it('decoder works on pairs', () => {
48+
// and in doing so, works on pairs, lists, etc...
49+
function $65$() {}
50+
const pair = cons($65$, 'placeholder')
51+
expect(decodeValue(pair).toString()).toEqual(`function A() { },placeholder`)
52+
})
53+
54+
it('decoder works on vectors', () => {
55+
// scheme doesn't support optimisation of circular vectors yet
56+
function $65$() {}
57+
const vector = [$65$, 2, 3]
58+
expect(decodeValue(vector).toString()).toEqual(`function A() { },2,3`)
59+
})
60+
61+
it('decodes runtime errors properly', () => {
62+
const token = `😀`
63+
const dummyNode: Node = dummyExpression()
64+
const error = new UnassignedVariable(encode(token), dummyNode)
65+
expect(decodeError(error).elaborate()).toContain(`😀`)
66+
})
67+
})

‎src/parser/__tests__/scheme.ts ‎src/alt-langs/scheme/__tests__/scheme-parser.ts

+21-23
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { parseError } from '../..'
2-
import { mockContext } from '../../mocks/context'
3-
import { Chapter } from '../../types'
4-
import { SchemeParser } from '../scheme'
5-
6-
const parser = new SchemeParser(Chapter.SCHEME_1)
1+
import { parseError } from '../../..'
2+
import { mockContext } from '../../../mocks/context'
3+
import { Chapter } from '../../../types'
4+
import { SchemeParser } from '../../../parser/scheme'
5+
6+
const parser_1 = new SchemeParser(Chapter.SCHEME_1)
7+
const parser_2 = new SchemeParser(Chapter.SCHEME_2)
8+
const parser_3 = new SchemeParser(Chapter.SCHEME_3)
9+
const parser_4 = new SchemeParser(Chapter.SCHEME_4)
710
const parser_full = new SchemeParser(Chapter.FULL_SCHEME)
811
let context = mockContext(Chapter.SCHEME_1)
912
let context_full = mockContext(Chapter.FULL_SCHEME)
@@ -15,7 +18,7 @@ beforeEach(() => {
1518

1619
describe('Scheme parser', () => {
1720
it('represents itself correctly', () => {
18-
expect(parser.toString()).toMatchInlineSnapshot(`"SchemeParser{chapter: 1}"`)
21+
expect(parser_1.toString()).toMatchInlineSnapshot(`"SchemeParser{chapter: 1}"`)
1922
})
2023

2124
it('throws error if given chapter is wrong', () => {
@@ -26,13 +29,13 @@ describe('Scheme parser', () => {
2629

2730
it('throws errors if option throwOnError is selected + parse error is encountered', () => {
2831
const code = `(hello))`
29-
expect(() => parser.parse(code, context, undefined, true)).toThrow("Unexpected ')'")
32+
expect(() => parser_1.parse(code, context, undefined, true)).toThrow("Unexpected ')'")
3033
})
3134

3235
it('formats tokenizer errors correctly', () => {
3336
const code = `(hello))`
3437

35-
parser.parse(code, context)
38+
parser_1.parse(code, context)
3639
expect(context.errors.slice(-1)[0]).toMatchObject(
3740
expect.objectContaining({ message: expect.stringContaining("Unexpected ')'") })
3841
)
@@ -41,45 +44,40 @@ describe('Scheme parser', () => {
4144
it('formats parser errors correctly', () => {
4245
const code = `(define (f x)`
4346

44-
parser.parse(code, context)
47+
parser_1.parse(code, context)
4548
expect(context.errors.slice(-1)[0]).toMatchObject(
4649
expect.objectContaining({ message: expect.stringContaining('Unexpected EOF') })
4750
)
4851
})
4952

50-
it('allows usage of builtins/preludes', () => {
51-
const code = `
52-
(+ 1 2 3)
53-
(gcd 10 15)
54-
`
55-
56-
parser.parse(code, context)
57-
expect(parseError(context.errors)).toMatchInlineSnapshot(`""`)
58-
})
59-
6053
it('allows usage of imports/modules', () => {
6154
const code = `(import "rune" (show heart))
6255
(show heart)
6356
`
6457

65-
parser.parse(code, context)
58+
parser_1.parse(code, context)
6659
expect(parseError(context.errors)).toMatchInlineSnapshot(`""`)
6760
})
6861

6962
it('disallows syntax for higher chapters', () => {
7063
const code = `'(1 2 3)`
7164

72-
parser.parse(code, context)
65+
parser_1.parse(code, context)
7366
expect(context.errors.slice(-1)[0]).toMatchObject(
7467
expect.objectContaining({
7568
message: expect.stringContaining("Syntax ''' not allowed at Scheme §1")
7669
})
7770
)
7871
})
7972

80-
it('allows syntax for lower chapters', () => {
73+
it('allows syntax for chapters of required or higher chapter', () => {
8174
const code = `'(1 2 3)`
8275

76+
// regardless of how many times we parse this code in the same context,
77+
// there should be no errors in the context as long as the chapter is 2 or higher
78+
parser_2.parse(code, context_full)
79+
parser_3.parse(code, context_full)
80+
parser_4.parse(code, context_full)
8381
parser_full.parse(code, context_full)
8482
expect(parseError(context_full.errors)).toMatchInlineSnapshot(`""`)
8583
})

‎src/alt-langs/scheme/scm-slang

Submodule scm-slang added at ad3b2e3

‎src/createContext.ts

+136-57
Large diffs are not rendered by default.

‎src/cse-machine/__tests__/__snapshots__/cse-machine-callcc.ts.snap

+8-36
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ Object {
1111
"displayResult": Array [],
1212
"numErrors": 0,
1313
"parsedErrors": "",
14-
"result": 10,
14+
"result": SchemeInteger {
15+
"numberType": 1,
16+
"value": 10n,
17+
},
1518
"resultStatus": "finished",
1619
"visualiseListResult": Array [],
1720
}
@@ -54,7 +57,10 @@ Object {
5457
"displayResult": Array [],
5558
"numErrors": 0,
5659
"parsedErrors": "",
57-
"result": 2,
60+
"result": SchemeInteger {
61+
"numberType": 1,
62+
"value": 2n,
63+
},
5864
"resultStatus": "finished",
5965
"visualiseListResult": Array [],
6066
}
@@ -92,37 +98,3 @@ Object {
9298
"visualiseListResult": Array [],
9399
}
94100
`;
95-
96-
exports[`cont throws error given >1 argument: expectParsedError 1`] = `
97-
Object {
98-
"alertResult": Array [],
99-
"code": "
100-
(+ 1 2 (call/cc
101-
(lambda (k) (k 3 'wrongwrongwrong!)))
102-
4)
103-
",
104-
"displayResult": Array [],
105-
"numErrors": 1,
106-
"parsedErrors": "Line 3: Expected 1 arguments, but got 2.",
107-
"result": undefined,
108-
"resultStatus": "error",
109-
"visualiseListResult": Array [],
110-
}
111-
`;
112-
113-
exports[`cont throws error given no arguments: expectParsedError 1`] = `
114-
Object {
115-
"alertResult": Array [],
116-
"code": "
117-
(+ 1 2 (call/cc
118-
(lambda (k) (k)))
119-
4)
120-
",
121-
"displayResult": Array [],
122-
"numErrors": 1,
123-
"parsedErrors": "Line 3: Expected 1 arguments, but got 0.",
124-
"result": undefined,
125-
"resultStatus": "error",
126-
"visualiseListResult": Array [],
127-
}
128-
`;

‎src/cse-machine/__tests__/cse-machine-callcc.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ test('basic call/cc works', () => {
1313
4)
1414
`,
1515
optionECScm
16-
).toMatchInlineSnapshot(`10`)
16+
).toMatchInlineSnapshot(`
17+
SchemeInteger {
18+
"numberType": 1,
19+
"value": 10n,
20+
}
21+
`)
1722
})
1823

1924
test('call/cc can be used to escape a computation', () => {
@@ -28,7 +33,12 @@ test('call/cc can be used to escape a computation', () => {
2833
test
2934
`,
3035
optionECScm
31-
).toMatchInlineSnapshot(`2`)
36+
).toMatchInlineSnapshot(`
37+
SchemeInteger {
38+
"numberType": 1,
39+
"value": 2n,
40+
}
41+
`)
3242
})
3343

3444
test('call/cc throws error given no arguments', () => {
@@ -52,6 +62,10 @@ test('call/cc throws error given >1 argument', () => {
5262
).toMatchInlineSnapshot(`"Line 2: Expected 1 arguments, but got 2."`)
5363
})
5464

65+
/*
66+
for now, continuations have variable arity but are unable to check for the "correct"
67+
number of arguments. we will omit these tests for now
68+
5569
test('cont throws error given no arguments', () => {
5670
return expectParsedError(
5771
`
@@ -73,7 +87,7 @@ test('cont throws error given >1 argument', () => {
7387
optionECScm
7488
).toMatchInlineSnapshot(`"Line 3: Expected 1 arguments, but got 2."`)
7589
})
76-
90+
*/
7791
test('call/cc can be stored as a value', () => {
7892
return expectResult(
7993
`

‎src/cse-machine/continuations.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export function getContinuationEnv(cn: Continuation): Environment[] {
5050

5151
export function makeContinuation(control: Control, stash: Stash, env: Environment[]): Function {
5252
// Cast a function into a continuation
53-
const fn: any = (x: any) => x
53+
// a continuation may take any amount of arguments
54+
const fn: Function = (...x: any[]) => x
5455
const cn: Continuation = fn as Continuation
5556

5657
// Set the control, stash and environment

‎src/cse-machine/instrCreator.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ export const genContInstr = (srcNode: Node): GenContInstr => ({
144144
srcNode
145145
})
146146

147-
export const resumeContInstr = (srcNode: Node): ResumeContInstr => ({
147+
export const resumeContInstr = (numOfArgs: number, srcNode: es.Node): ResumeContInstr => ({
148+
numOfArgs: numOfArgs,
148149
instrType: InstrType.RESUME_CONT,
149150
srcNode
150151
})

‎src/cse-machine/interpreter.ts

+52-11
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,10 @@
4848
CseError,
4949
EnvInstr,
5050
ForInstr,
51+
GenContInstr,
5152
Instr,
5253
InstrType,
54+
ResumeContInstr,
5355
UnOpInstr,
5456
WhileInstr
5557
} from './types'
@@ -248,7 +250,7 @@
248250
* @returns The corresponding promise.
249251
*/
250252
export function CSEResultPromise(context: Context, value: Value): Promise<Result> {
251253
return new Promise((resolve, reject) => {
252254
if (value instanceof CSEBreak) {
253255
resolve({ status: 'suspended-cse-eval', context })
254256
} else if (value instanceof CseError) {
@@ -397,6 +399,42 @@
397399
declareFunctionsAndVariables(context, command, environment)
398400
}
399401

402+
// A strange bug occurs here when successive REPL commands are run, as they
403+
// are each evaluated as separate programs. This causes the environment to be
404+
// pushed multiple times.
405+
406+
// As such, we need to "append" the tail environment to the current environment
407+
// if and only if the tail environment is a previous program environment.
408+
409+
const currEnv = currentEnvironment(context)
410+
if (
411+
currEnv &&
412+
currEnv.name === 'programEnvironment' &&
413+
currEnv.tail &&
414+
currEnv.tail.name === 'programEnvironment'
415+
) {
416+
// we need to take that tail environment and append its items to the current environment
417+
const oldEnv = currEnv.tail
418+
419+
// separate the tail environment from the environments list
420+
currEnv.tail = oldEnv.tail
421+
422+
// we will recycle the old environment's item list
423+
// add the items from the current environment to the tail environment
424+
// this is fine, especially as the older program will never
425+
// need to use the old environment's items again
426+
for (const key in currEnv.head) {
427+
oldEnv.head[key] = currEnv.head[key]
428+
}
429+
430+
// set the current environment to the old one
431+
// this will work across successive programs as well
432+
433+
// this will also allow continuations to read newer program
434+
// values from their "outdated" program environment
435+
currEnv.head = oldEnv.head
436+
}
437+
400438
if (command.body.length == 1) {
401439
// If program only consists of one statement, evaluate it immediately
402440
const next = command.body[0]
@@ -450,7 +488,7 @@
450488
command: es.WhileStatement,
451489
context: Context,
452490
control: Control,
453491
stash: Stash
454492
) {
455493
if (hasBreakStatement(command.body as es.BlockStatement)) {
456494
control.push(instr.breakMarkerInstr(command))
@@ -533,7 +571,7 @@
533571
command: es.IfStatement,
534572
context: Context,
535573
control: Control,
536574
stash: Stash
537575
) {
538576
control.push(...reduceConditional(command))
539577
},
@@ -606,7 +644,7 @@
606644
command: es.ContinueStatement,
607645
context: Context,
608646
control: Control,
609647
stash: Stash
610648
) {
611649
control.push(instr.contInstr(command))
612650
},
@@ -615,7 +653,7 @@
615653
command: es.BreakStatement,
616654
context: Context,
617655
control: Control,
618656
stash: Stash
619657
) {
620658
control.push(instr.breakInstr(command))
621659
},
@@ -662,7 +700,7 @@
662700
command: es.MemberExpression,
663701
context: Context,
664702
control: Control,
665703
stash: Stash
666704
) {
667705
control.push(instr.arrAccInstr(command))
668706
control.push(command.property)
@@ -673,7 +711,7 @@
673711
command: es.ConditionalExpression,
674712
context: Context,
675713
control: Control,
676714
stash: Stash
677715
) {
678716
control.push(...reduceConditional(command))
679717
},
@@ -744,7 +782,7 @@
744782
* Instructions
745783
*/
746784

747785
[InstrType.RESET]: function (command: Instr, context: Context, control: Control, stash: Stash) {
748786
// Keep pushing reset instructions until marker is found.
749787
const cmdNext: ControlItem | undefined = control.pop()
750788
if (cmdNext && (isNode(cmdNext) || cmdNext.instrType !== InstrType.MARKER)) {
@@ -914,17 +952,16 @@
914952
// Check for number of arguments mismatch error
915953
checkNumberOfArguments(context, func, args, command.srcNode)
916954

917-
// A continuation is always given a single argument
918-
const expression: Value = args[0]
919-
920955
const dummyContCallExpression = makeDummyContCallExpression('f', 'cont')
921956

922957
// Restore the state of the stash,
923958
// but replace the function application instruction with
924959
// a resume continuation instruction
925960
stash.push(func)
926-
stash.push(expression)
927-
control.push(instr.resumeContInstr(dummyContCallExpression))
961+
// we need to push the arguments back onto the stash
962+
// as well
963+
stash.push(...args)
964+
control.push(instr.resumeContInstr(command.numOfArgs, dummyContCallExpression))
928965
return
929966
}
930967

@@ -1075,7 +1112,7 @@
10751112
command: Instr,
10761113
context: Context,
10771114
control: Control,
10781115
stash: Stash
10791116
) {
10801117
const next = control.pop() as ControlItem
10811118
if (isInstr(next) && next.instrType == InstrType.CONTINUE_MARKER) {
@@ -1091,7 +1128,7 @@
10911128

10921129
[InstrType.CONTINUE_MARKER]: function () {},
10931130

10941131
[InstrType.BREAK]: function (command: Instr, context: Context, control: Control, stash: Stash) {
10951132
const next = control.pop() as ControlItem
10961133
if (isInstr(next) && next.instrType == InstrType.BREAK_MARKER) {
10971134
// Encountered break mark, stop popping
@@ -1107,7 +1144,7 @@
11071144
[InstrType.BREAK_MARKER]: function () {},
11081145

11091146
[InstrType.GENERATE_CONT]: function (
1110-
_command: Instr,
1147+
_command: GenContInstr,
11111148
context: Context,
11121149
control: Control,
11131150
stash: Stash
@@ -1128,12 +1165,16 @@
11281165
},
11291166

11301167
[InstrType.RESUME_CONT]: function (
1131-
_command: Instr,
1168+
command: ResumeContInstr,
11321169
context: Context,
11331170
control: Control,
11341171
stash: Stash
11351172
) {
1136-
const expression = stash.pop()
1173+
// pop the arguments
1174+
const args: Value[] = []
1175+
for (let i = 0; i < command.numOfArgs; i++) {
1176+
args.unshift(stash.pop())
1177+
}
11371178
const cn: Continuation = stash.pop() as Continuation
11381179

11391180
const contControl = getContinuationControl(cn)
@@ -1144,10 +1185,10 @@
11441185
control.setTo(contControl)
11451186
stash.setTo(contStash)
11461187

1147-
// Push the expression given to the continuation onto the stash
1148-
stash.push(expression)
1188+
// Push the arguments given to the continuation back onto the stash
1189+
stash.push(...args)
11491190

1150-
// Restore the environment pointer to that of the continuation
1191+
// Restore the environment pointer to that of the continuation's environment
11511192
context.runtime.environments = contEnv
11521193
}
11531194
}

‎src/cse-machine/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ export interface ArrLitInstr extends BaseInstr {
7777

7878
export type GenContInstr = BaseInstr
7979

80-
export type ResumeContInstr = BaseInstr
80+
export interface ResumeContInstr extends BaseInstr {
81+
numOfArgs: number
82+
}
8183

8284
export type Instr =
8385
| BaseInstr

‎src/cse-machine/utils.ts

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { RuntimeSourceError } from '../errors/runtimeSourceError'
77
import Closure from '../interpreter/closure'
88
import { Environment, Frame, Node, StatementSequence, Value } from '../types'
99
import * as ast from '../utils/astCreator'
10+
import { isContinuation } from './continuations'
1011
import * as instr from './instrCreator'
1112
import { Control } from './interpreter'
1213
import { AppInstr, AssmtInstr, ControlItem, Instr, InstrType } from './types'
@@ -490,6 +491,12 @@ export const checkNumberOfArguments = (
490491
)
491492
)
492493
}
494+
} else if (isContinuation(callee)) {
495+
// Continuations have variadic arguments,
496+
// and so we can let it pass
497+
// in future, if we can somehow check the number of arguments
498+
// expected by the continuation, we can add a check here.
499+
return undefined
493500
} else {
494501
// Pre-built functions
495502
const hasVarArgs = callee.minArgsNeeded != undefined

‎src/parser/__tests__/scheme-encode-decode.ts

-45
This file was deleted.

‎src/parser/scheme/index.ts

+64-24
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
import { Node, Program } from 'estree'
1+
import { Program } from 'estree'
22

3-
import { decode, encode, schemeParse } from '../../scm-slang/src'
4-
import { Pair } from '../../scm-slang/src/stdlib/source-scheme-library'
3+
import { decode, schemeParse } from '../../alt-langs/scheme/scm-slang/src'
4+
import {
5+
car,
6+
cdr,
7+
circular$45$list$63$,
8+
cons,
9+
last$45$pair,
10+
list$45$tail,
11+
pair$63$,
12+
procedure$63$,
13+
set$45$cdr$33$,
14+
vector$63$
15+
} from '../../alt-langs/scheme/scm-slang/src/stdlib/base'
16+
import { List, Pair } from '../../stdlib/list'
517
import { Chapter, Context, ErrorType, SourceError } from '../../types'
618
import { FatalSyntaxError } from '../errors'
719
import { AcornOptions, Parser } from '../types'
820
import { positionToSourceLocation } from '../utils'
9-
const walk = require('acorn-walk')
1021

1122
export class SchemeParser implements Parser<AcornOptions> {
1223
private chapter: number
@@ -21,10 +32,8 @@ export class SchemeParser implements Parser<AcornOptions> {
2132
): Program | null {
2233
try {
2334
// parse the scheme code
24-
const estree = schemeParse(programStr, this.chapter)
25-
// walk the estree and encode all identifiers
26-
encodeTree(estree)
27-
return estree as unknown as Program
35+
const estree = schemeParse(programStr, this.chapter, true)
36+
return estree as Program
2837
} catch (error) {
2938
if (error instanceof SyntaxError) {
3039
error = new FatalSyntaxError(positionToSourceLocation((error as any).loc), error.toString())
@@ -63,15 +72,6 @@ function getSchemeChapter(chapter: Chapter): number {
6372
}
6473
}
6574

66-
export function encodeTree(tree: Program): Program {
67-
walk.full(tree, (node: Node) => {
68-
if (node.type === 'Identifier') {
69-
node.name = encode(node.name)
70-
}
71-
})
72-
return tree
73-
}
74-
7575
function decodeString(str: string): string {
7676
return str.replace(/\$scheme_[\w$]+|\$\d+\$/g, match => {
7777
return decode(match)
@@ -80,18 +80,58 @@ function decodeString(str: string): string {
8080

8181
// Given any value, decode it if and
8282
// only if an encoded value may exist in it.
83+
// this function is used to accurately display
84+
// values in the REPL.
8385
export function decodeValue(x: any): any {
84-
// In future: add support for decoding vectors.
85-
if (x instanceof Pair) {
86+
// helper version of list_tail that assumes non-null return value
87+
function list_tail(xs: List, i: number): List {
88+
if (i === 0) {
89+
return xs
90+
} else {
91+
return list_tail(list$45$tail(xs), i - 1)
92+
}
93+
}
94+
95+
if (circular$45$list$63$(x)) {
8696
// May contain encoded strings.
87-
return new Pair(decodeValue(x.car), decodeValue(x.cdr))
88-
} else if (x instanceof Array) {
97+
let circular_pair_index = -1
98+
const all_pairs: Pair<any, any>[] = []
99+
100+
// iterate through all pairs in the list until we find the circular pair
101+
let current = x
102+
while (current !== null) {
103+
if (all_pairs.includes(current)) {
104+
circular_pair_index = all_pairs.indexOf(current)
105+
break
106+
}
107+
all_pairs.push(current)
108+
current = cdr(current)
109+
}
110+
x
111+
// assemble a new list using the elements in all_pairs
112+
let new_list = null
113+
for (let i = all_pairs.length - 1; i >= 0; i--) {
114+
new_list = cons(decodeValue(car(all_pairs[i])), new_list)
115+
}
116+
117+
// finally we can set the last cdr of the new list to the circular-pair itself
118+
119+
const circular_pair = list_tail(new_list, circular_pair_index)
120+
set$45$cdr$33$(last$45$pair(new_list), circular_pair)
121+
return new_list
122+
} else if (pair$63$(x)) {
123+
// May contain encoded strings.
124+
return cons(decodeValue(car(x)), decodeValue(cdr(x)))
125+
} else if (vector$63$(x)) {
89126
// May contain encoded strings.
90127
return x.map(decodeValue)
91-
} else if (x instanceof Function) {
128+
} else if (procedure$63$(x)) {
129+
// copy x to avoid modifying the original object
130+
const newX = { ...x }
92131
const newString = decodeString(x.toString())
93-
x.toString = () => newString
94-
return x
132+
// change the toString method to return the decoded string
133+
newX.toString = () => newString
134+
return newX
95135
} else {
96136
// string, number, boolean, null, undefined
97137
// no need to decode.

‎src/scm-slang

-1
This file was deleted.

‎tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"outDir": "./dist",
44
"module": "commonjs",
55
"declaration": true,
6-
"target": "es2016",
6+
"target": "es2020",
77
"lib": [
88
"es2021.string",
99
"es2018",

0 commit comments

Comments
 (0)
Please sign in to comment.