• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / js-slang / 11929582114

20 Nov 2024 08:33AM UTC coverage: 81.654% (+0.04%) from 81.61%
11929582114

Pull #1728

github

web-flow
Merge c3f0269a5 into 665233634
Pull Request #1728: Implement the CSET machine, along with macros

3654 of 4864 branches covered (75.12%)

Branch coverage included in aggregate %.

491 of 596 new or added lines in 11 files covered. (82.38%)

1 existing line in 1 file now uncovered.

11474 of 13663 relevant lines covered (83.98%)

143610.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.7
/src/cse-machine/scheme-macros.ts
1
import * as es from 'estree'
2
import * as errors from '../errors/errors'
79✔
3
import { List } from '../stdlib/list'
4
import { _Symbol } from '../alt-langs/scheme/scm-slang/src/stdlib/base'
79✔
5
import { is_number, SchemeNumber } from '../alt-langs/scheme/scm-slang/src/stdlib/core-math'
79✔
6
import { Context } from '..'
7
import { Control, Stash } from './interpreter'
8
import { currentTransformers, getVariable, handleRuntimeError } from './utils'
79✔
9
import { Transformer, macro_transform, match } from './patterns'
79✔
10
import {
79✔
11
  arrayToImproperList,
12
  arrayToList,
13
  flattenImproperList,
14
  isImproperList
15
} from './macro-utils'
16
import { ControlItem } from './types'
17
import { encode } from '../alt-langs/scheme/scm-slang/src'
79✔
18
import { popInstr } from './instrCreator'
79✔
19
import { flattenList, isList } from './macro-utils'
79✔
20

21
// this needs to be better but for now it's fine
22
export type SchemeControlItems = List | _Symbol | SchemeNumber | boolean | string
23

24
/**
25
 * A metaprocedure used to detect for the apply function object.
26
 * If the interpreter sees this specific function,
27
 * it will take all of the operands, and apply the second to second last operands as well as the last operand (must be a list)
28
 * to the first operand (which must be a function).
29
 */
30
export class Apply extends Function {
79✔
31
  private static instance: Apply = new Apply()
79✔
32

33
  private constructor() {
34
    super()
79✔
35
  }
36

37
  public static get(): Apply {
38
    return Apply.instance
79✔
39
  }
40

41
  public toString(): string {
NEW
42
    return 'apply'
×
43
  }
44
}
45

46
export const cset_apply = Apply.get()
79✔
47

48
export function isApply(value: any): boolean {
79✔
49
  return value === cset_apply
656,648✔
50
}
51

52
/**
53
 * A metaprocedure used to detect for the eval function object.
54
 * If the interpreter sees this specific function,
55
 * it will transfer its operand to the control component.
56
 */
57
export class Eval extends Function {
79✔
58
  private static instance: Eval = new Eval()
79✔
59

60
  private constructor() {
61
    super()
79✔
62
  }
63

64
  public static get(): Eval {
65
    return Eval.instance
79✔
66
  }
67

68
  public toString(): string {
NEW
69
    return 'eval'
×
70
  }
71
}
72

73
export const cset_eval = Eval.get()
79✔
74

75
export function isEval(value: any): boolean {
79✔
76
  return value === cset_eval
657,800✔
77
}
78

79
function isSpecialForm(sym: _Symbol): boolean {
80
  return [
1,010✔
81
    'lambda',
82
    'define',
83
    'set!',
84
    'if',
85
    'begin',
86
    'quote',
87
    'quasiquote',
88
    'define-syntax',
89
    'syntax-rules',
90
    'eval'
91
  ].includes(sym.sym)
92
}
93

94
export function schemeEval(
79✔
95
  command: SchemeControlItems,
96
  context: Context,
97
  control: Control,
98
  stash: Stash,
99
  isPrelude: boolean
100
) {
101
  const transformers = currentTransformers(context)
2,894✔
102
  // scheme CSE machine will only ever encounter
103
  // lists or primitives like symbols, booleans or strings.
104
  // if its a list, we can parse the list and evaluate each item as necessary
105
  // if its a symbol, we can look up the symbol in the environment.
106
  // for either of these operations, if our list matches some pattern in
107
  // the T component, then we can apply the corresponding rule.
108

109
  // if its a number, boolean, or string, we can just shift the value
110
  // onto the stash.
111

112
  if (command === null) {
2,894✔
113
    return handleRuntimeError(context, new errors.ExceptionError(new Error('Cannot evaluate null')))
2✔
114
  }
115

116
  if (isList(command)) {
2,892✔
117
    // do something
118
    const parsedList = flattenList(command)
2,635✔
119
    const elem = parsedList[0]
2,635✔
120
    // do work based on the first element of the list.
121
    // it should match some symbol "define", "set", "lambda", etc...
122
    // or if it doesn't match any of these, then it is a function call.
123
    if (elem instanceof _Symbol) {
2,635✔
124
      // check if elem matches any defined syntax in the T component.
125
      // if it does, then apply the corresponding rule.
126
      if (transformers.hasPattern(elem.sym)) {
2,634✔
127
        // get the relevant transformers
128
        const transformerList: Transformer[] = transformers.getPattern(elem.sym)
11✔
129

130
        // find the first matching transformer
131
        for (const transformer of transformerList) {
11✔
132
          // check if the transformer matches the list
133
          try {
19✔
134
            if (match(command, transformer.pattern, transformer.literals)) {
19✔
135
              // if it does, apply the transformer
136
              const transformedMacro = macro_transform(command as List, transformer)
10✔
137
              control.push(transformedMacro as ControlItem)
10✔
138
              return
10✔
139
            }
140
          } catch (e) {
NEW
141
            return handleRuntimeError(
×
142
              context,
143
              new errors.ExceptionError(
144
                new Error(
145
                  'Error in macro-expanding ' +
146
                    elem.sym +
147
                    '! Are the template and pattern well formed?'
148
                )
149
              )
150
            )
151
          }
152
        }
153

154
        // there is an error if we get to here
155
        return handleRuntimeError(
1✔
156
          context,
157
          new errors.ExceptionError(
158
            new Error('No matching transformer found for macro ' + elem.sym)
159
          )
160
        )
161
      }
162

163
      // else, this is a standard special form.
164
      // we attempt to piggyback on the standard CSE machine to
165
      // handle the basic special forms.
166
      // however, for more advanced stuff like quotes or definitions,
167
      // the logic will be handled here.
168
      switch (parsedList[0].sym) {
2,623✔
169
        case 'lambda':
170
          if (parsedList.length < 3) {
709!
NEW
171
            return handleRuntimeError(
×
172
              context,
173
              new errors.ExceptionError(new Error('lambda requires at least 2 arguments!'))
174
            )
175
          }
176
          // return a lambda expression that takes
177
          // in the arguments, and returns the body
178
          // as an eval of the body.
179
          const args = parsedList[1]
709✔
180

181
          let argsList: _Symbol[] = []
709✔
182
          let rest: _Symbol | null = null
709✔
183
          if (args instanceof _Symbol) {
709!
184
            // if the args is a symbol, then it is a variadic function.
185
            // we can just set the args to a list of the symbol.
NEW
186
            rest = args
×
187
          } else if (isImproperList(args)) {
709✔
188
            ;[argsList, rest] = flattenImproperList(args)
235✔
189
            argsList.forEach((arg: any) => {
235✔
190
              if (!(arg instanceof _Symbol)) {
611!
NEW
191
                return handleRuntimeError(
×
192
                  context,
193
                  new errors.ExceptionError(new Error('Invalid arguments for lambda!'))
194
                )
195
              }
196
              return
611✔
197
            })
198
            if (rest !== null && !(rest instanceof _Symbol)) {
235!
NEW
199
              return handleRuntimeError(
×
200
                context,
201
                new errors.ExceptionError(new Error('Invalid arguments for lambda!'))
202
              )
203
            }
204
          } else if (isList(args)) {
474!
205
            argsList = flattenList(args) as _Symbol[]
474✔
206
            argsList.forEach((arg: any) => {
474✔
207
              if (!(arg instanceof _Symbol)) {
1,368✔
208
                return handleRuntimeError(
2✔
209
                  context,
210
                  new errors.ExceptionError(new Error('Invalid arguments for lambda!'))
211
                )
212
              }
213
              return
1,366✔
214
            })
215
          } else {
NEW
216
            return handleRuntimeError(
×
217
              context,
218
              new errors.ExceptionError(new Error('Invalid arguments for lambda!'))
219
            )
220
          }
221

222
          // convert the args to estree pattern
223
          const params: (es.Identifier | es.RestElement)[] = argsList.map(arg =>
707✔
224
            makeDummyIdentifierNode(encode(arg.sym))
1,977✔
225
          )
226

227
          let body_elements = parsedList.slice(2)
707✔
228
          let body: List = arrayToList([new _Symbol('begin'), ...body_elements])
707✔
229

230
          // if there is a rest argument, we need to wrap it in a rest element.
231
          // we also need to add another element to the body,
232
          // to convert the rest element into a list.
233
          if (rest !== null) {
707✔
234
            params.push({
235✔
235
              type: 'RestElement',
236
              argument: makeDummyIdentifierNode(encode(rest.sym))
237
            })
238
            body = arrayToList([
235✔
239
              new _Symbol('begin'),
240
              arrayToList([
241
                new _Symbol('set!'),
242
                rest,
243
                arrayToList([new _Symbol('vector->list'), rest])
244
              ]),
245
              ...body_elements
246
            ])
247
          }
248

249
          // estree ArrowFunctionExpression
250
          const lambda = {
707✔
251
            type: 'ArrowFunctionExpression',
252
            params: params,
253
            body: convertToEvalExpression(body)
254
          }
255

256
          control.push(lambda as es.ArrowFunctionExpression)
707✔
257
          return
707✔
258

259
        case 'define':
260
          if (parsedList.length < 3) {
1,565✔
261
            return handleRuntimeError(
1✔
262
              context,
263
              new errors.ExceptionError(new Error('define requires at least 2 arguments!'))
264
            )
265
          }
266
          const variable = parsedList[1]
1,564✔
267
          if (isList(variable)) {
1,564✔
268
            // then this define is actually a function definition
269
            const varList = flattenList(variable)
472✔
270
            const name = varList[0]
472✔
271
            const params = varList.slice(1)
472✔
272
            const body = parsedList.slice(2)
472✔
273

274
            const define_function = arrayToList([
472✔
275
              new _Symbol('define'),
276
              name,
277
              arrayToList([new _Symbol('lambda'), arrayToList(params), ...body])
278
            ])
279
            control.push(define_function as any)
472✔
280
            return
472✔
281
          } else if (isImproperList(variable)) {
1,092✔
282
            const [varList, rest] = flattenImproperList(variable)
235✔
283
            const name = varList[0]
235✔
284
            const params = varList.slice(1)
235✔
285
            const body = parsedList.slice(2)
235✔
286

287
            const define_function = arrayToList([
235✔
288
              new _Symbol('define'),
289
              name,
290
              arrayToList([new _Symbol('lambda'), arrayToImproperList(params, rest), ...body])
291
            ])
292
            control.push(define_function as any)
235✔
293
            return
235✔
294
          } else if (!(variable instanceof _Symbol)) {
857!
NEW
295
            return handleRuntimeError(
×
296
              context,
297
              new errors.ExceptionError(new Error('Invalid variable for define!'))
298
            )
299
          }
300
          if (parsedList.length !== 3) {
857✔
301
            return handleRuntimeError(
1✔
302
              context,
303
              new errors.ExceptionError(new Error('define requires 2 arguments!'))
304
            )
305
          }
306
          // make sure variable does not shadow a special form
307
          if (isSpecialForm(variable) && !isPrelude) {
856✔
308
            return handleRuntimeError(
1✔
309
              context,
310
              new errors.ExceptionError(
311
                new Error('Cannot shadow special form ' + variable.sym + ' with a definition!')
312
              )
313
            )
314
          }
315
          const value = parsedList[2]
855✔
316
          // estree VariableDeclaration
317
          const definition = {
855✔
318
            type: 'VariableDeclaration',
319
            kind: 'let',
320
            declarations: [
321
              {
322
                type: 'VariableDeclarator',
323
                id: makeDummyIdentifierNode(encode(variable.sym)),
324
                init: convertToEvalExpression(value)
325
              }
326
            ]
327
          }
328

329
          control.push(definition as es.VariableDeclaration)
855✔
330
          return
855✔
331
        case 'set!':
332
          if (parsedList.length !== 3) {
4✔
333
            return handleRuntimeError(
2✔
334
              context,
335
              new errors.ExceptionError(new Error('set! requires 2 arguments!'))
336
            )
337
          }
338
          const set_variable = parsedList[1]
2✔
339
          if (!(set_variable instanceof _Symbol)) {
2!
NEW
340
            return handleRuntimeError(
×
341
              context,
342
              new errors.ExceptionError(new Error('Invalid arguments for set!'))
343
            )
344
          }
345
          // make sure set_variable does not shadow a special form
346
          if (isSpecialForm(set_variable) && !isPrelude) {
2✔
347
            return handleRuntimeError(
1✔
348
              context,
349
              new errors.ExceptionError(
350
                new Error('Cannot overwrite special form ' + set_variable.sym + ' with a value!')
351
              )
352
            )
353
          }
354
          const set_value = parsedList[2]
1✔
355

356
          // estree AssignmentExpression
357
          const assignment = {
1✔
358
            type: 'AssignmentExpression',
359
            operator: '=',
360
            left: makeDummyIdentifierNode(encode(set_variable.sym)),
361
            right: convertToEvalExpression(set_value)
362
          }
363

364
          control.push(assignment as es.AssignmentExpression)
1✔
365
          return
1✔
366
        case 'if':
367
          if (parsedList.length < 3) {
9✔
368
            return handleRuntimeError(
1✔
369
              context,
370
              new errors.ExceptionError(new Error('if requires at least 2 arguments!'))
371
            )
372
          }
373
          if (parsedList.length > 4) {
8✔
374
            return handleRuntimeError(
1✔
375
              context,
376
              new errors.ExceptionError(new Error('if requires at most 3 arguments!'))
377
            )
378
          }
379
          const condition = parsedList[1]
7✔
380
          const consequent = parsedList[2]
7✔
381
          // check if there is an alternate
382
          const alternate = parsedList.length > 3 ? parsedList[3] : undefined
7✔
383

384
          // evaluate the condition with truthy
385
          const truthyCondition = arrayToList([new _Symbol('truthy'), condition])
7✔
386

387
          // estree ConditionalExpression
388
          const conditional = {
7✔
389
            type: 'ConditionalExpression',
390
            test: convertToEvalExpression(truthyCondition),
391
            consequent: convertToEvalExpression(consequent),
392
            alternate: alternate ? convertToEvalExpression(alternate) : undefined
7✔
393
          }
394

395
          control.push(conditional as es.ConditionalExpression)
7✔
396
          return
7✔
397
        case 'begin':
398
          // begin is a sequence of expressions
399
          // that are evaluated in order.
400
          // push the expressions to the control in reverse
401
          // order.
402
          if (parsedList.length < 2) {
62✔
403
            return handleRuntimeError(
1✔
404
              context,
405
              new errors.ExceptionError(new Error('begin requires at least 1 argument!'))
406
            )
407
          }
408

409
          control.push(parsedList[parsedList.length - 1])
61✔
410
          for (let i = parsedList.length - 2; i > 0; i--) {
61✔
411
            control.push(popInstr(makeDummyIdentifierNode('pop')))
960✔
412
            control.push(parsedList[i])
960✔
413
          }
414
          return
61✔
415
        case 'quote':
416
          // quote is a special form that returns the expression
417
          // as is, without evaluating it.
418
          // we can just push the expression to the stash.
419
          if (parsedList.length !== 2) {
46✔
420
            return handleRuntimeError(
2✔
421
              context,
422
              new errors.ExceptionError(new Error('quote requires 1 argument!'))
423
            )
424
          }
425
          stash.push(parsedList[1])
44✔
426
          return
44✔
427
        // case 'quasiquote': quasiquote is implemented as a macro.
428
        case 'define-syntax':
429
          if (parsedList.length !== 3) {
155✔
430
            return handleRuntimeError(
2✔
431
              context,
432
              new errors.ExceptionError(new Error('define-syntax requires 2 arguments!'))
433
            )
434
          }
435
          // parse the pattern and template here,
436
          // generate a list of transformers from it,
437
          // and add it to the Patterns component.
438
          const syntaxName = parsedList[1]
153✔
439
          if (!(syntaxName instanceof _Symbol)) {
153✔
440
            return handleRuntimeError(
1✔
441
              context,
442
              new errors.ExceptionError(new Error('define-syntax requires a symbol!'))
443
            )
444
          }
445
          // make sure syntaxName does not shadow a special form
446
          if (isSpecialForm(syntaxName) && !isPrelude) {
152✔
447
            return handleRuntimeError(
1✔
448
              context,
449
              new errors.ExceptionError(
450
                new Error('Cannot shadow special form ' + syntaxName.sym + ' with a macro!')
451
              )
452
            )
453
          }
454
          const syntaxRules = parsedList[2]
151✔
455
          if (!isList(syntaxRules)) {
151✔
456
            return handleRuntimeError(
1✔
457
              context,
458
              new errors.ExceptionError(
459
                new Error('define-syntax requires a syntax-rules transformer!')
460
              )
461
            )
462
          }
463

464
          // at this point, we assume that syntax-rules is verified
465
          // and parsed correctly already.
466
          const syntaxRulesList = flattenList(syntaxRules)
150✔
467
          if (
150✔
468
            !(syntaxRulesList[0] instanceof _Symbol) ||
300✔
469
            syntaxRulesList[0].sym !== 'syntax-rules'
470
          ) {
471
            return handleRuntimeError(
1✔
472
              context,
473
              new errors.ExceptionError(
474
                new Error('define-syntax requires a syntax-rules transformer!')
475
              )
476
            )
477
          }
478
          if (syntaxRulesList.length < 3) {
149✔
479
            return handleRuntimeError(
1✔
480
              context,
481
              new errors.ExceptionError(new Error('syntax-rules requires at least 2 arguments!'))
482
            )
483
          }
484
          if (!isList(syntaxRulesList[1])) {
148✔
485
            return handleRuntimeError(
1✔
486
              context,
487
              new errors.ExceptionError(new Error('Invalid syntax-rules literals!'))
488
            )
489
          }
490
          const literalList = flattenList(syntaxRulesList[1])
147✔
491
          const literals: string[] = literalList.map((literal: _Symbol) => {
147✔
492
            if (!(literal instanceof _Symbol)) {
143✔
493
              return handleRuntimeError(
1✔
494
                context,
495
                new errors.ExceptionError(new Error('Invalid syntax-rules literals!'))
496
              )
497
            }
498
            return literal.sym
142✔
499
          })
500
          const rules = syntaxRulesList.slice(2)
146✔
501
          // rules are set as a list of patterns and templates.
502
          // we need to convert these into transformers.
503
          const transformerList: Transformer[] = rules.map(rule => {
146✔
504
            if (!isList(rule)) {
477✔
505
              return handleRuntimeError(
1✔
506
                context,
507
                new errors.ExceptionError(new Error('Invalid syntax-rules rule!'))
508
              )
509
            }
510
            const ruleList = flattenList(rule)
476✔
511
            const pattern = ruleList[0]
476✔
512
            const template = ruleList[1]
476✔
513
            return new Transformer(literals, pattern, template)
476✔
514
          })
515
          // now we can add the transformers to the transformers component.
516
          transformers.addPattern(syntaxName.sym, transformerList)
145✔
517

518
          // for consistency with scheme expecting undefined return values, push undefined to the stash.
519
          stash.push(undefined)
145✔
520
          return
145✔
521
        case 'syntax-rules':
522
          // syntax-rules is a special form that is used to define
523
          // a set of transformers.
524
          // it isn't meant to be evaluated outside of a define-syntax.
525
          // throw an error.
526
          return handleRuntimeError(
1✔
527
            context,
528
            new errors.ExceptionError(
529
              new Error('syntax-rules must only exist within define-syntax!')
530
            )
531
          )
532
      }
533
    }
534
    // if we get to this point, then it is a function call.
535
    // convert it into an es.CallExpression and push it to the control.
536
    const procedure = parsedList[0]
73✔
537
    const args = parsedList.slice(1)
73✔
538
    const appln = {
73✔
539
      type: 'CallExpression',
540
      optional: false,
541
      callee: convertToEvalExpression(procedure) as es.Expression,
542
      arguments: args.map(convertToEvalExpression) // unfortunately, each one needs to be converted.
543
    }
544
    control.push(appln as es.CallExpression)
73✔
545
    return
73✔
546
  } else if (command instanceof _Symbol) {
257✔
547
    // get the value of the symbol from the environment
548
    // associated with this symbol.
549
    const encodedName = encode(command.sym)
226✔
550
    stash.push(getVariable(context, encodedName, makeDummyIdentifierNode(command.sym)))
226✔
551
    return
226✔
552
  }
553
  // if we get to this point of execution, it is just some primitive value.
554
  // just push it to the stash.
555
  stash.push(command)
31✔
556
  return
31✔
557
}
558

559
export function makeDummyIdentifierNode(name: string): es.Identifier {
79✔
560
  return {
4,254✔
561
    type: 'Identifier',
562
    name
563
  }
564
}
565

566
/**
567
 * Convert a scheme expression (that is meant to be evaluated)
568
 * into an estree expression, using eval.
569
 * this will let us avoid the "hack" of storing Scheme lists
570
 * in estree nodes.
571
 * @param expression
572
 * @returns estree expression
573
 */
574
export function convertToEvalExpression(expression: SchemeControlItems): es.CallExpression {
79✔
575
  function convertToEstreeExpression(expression: SchemeControlItems): es.Expression {
576
    /*
577
    cases to consider:
578
    - list
579
    - pair/improper list
580
    - symbol
581
    - number
582
    - boolean
583
    - string
584
    */
585
    if (isList(expression)) {
38,923✔
586
      // make a call expression to list
587
      // with the elements of the list as its arguments.
588
      const args = flattenList(expression).map(convertToEstreeExpression)
13,311✔
589
      return {
13,311✔
590
        type: 'CallExpression',
591
        optional: false,
592
        callee: {
593
          type: 'Identifier',
594
          name: 'list'
595
        },
596
        arguments: args
597
      }
598
    } else if (isImproperList(expression)) {
25,612✔
599
      // make a call to cons
600
      // with the car and cdr as its arguments.
601
      const [car, cdr] = expression as [SchemeControlItems, SchemeControlItems]
611✔
602
      return {
611✔
603
        type: 'CallExpression',
604
        optional: false,
605
        callee: {
606
          type: 'Identifier',
607
          name: 'cons'
608
        },
609
        arguments: [convertToEstreeExpression(car), convertToEstreeExpression(cdr)]
610
      }
611
    } else if (expression instanceof _Symbol) {
25,001✔
612
      // make a call to string->symbol
613
      // with the symbol name as its argument.
614
      return {
24,818✔
615
        type: 'CallExpression',
616
        optional: false,
617
        callee: {
618
          type: 'Identifier',
619
          name: encode('string->symbol')
620
        },
621
        arguments: [
622
          {
623
            type: 'Literal',
624
            value: expression.sym
625
          }
626
        ]
627
      }
628
    } else if (is_number(expression)) {
183✔
629
      // make a call to string->number
630
      // with the number toString() as its argument.
631
      return {
166✔
632
        type: 'CallExpression',
633
        optional: false,
634
        callee: {
635
          type: 'Identifier',
636
          name: encode('string->number')
637
        },
638
        arguments: [
639
          {
640
            type: 'Literal',
641
            value: (expression as any).toString()
642
          }
643
        ]
644
      }
645
    }
646
    // if we're here, then it is a boolean or string.
647
    // just return the literal value.
648
    return {
17✔
649
      type: 'Literal',
650
      value: expression as boolean | string
651
    }
652
  }
653

654
  // make a call expression to eval with the single expression as its component.
655
  return {
1,737✔
656
    type: 'CallExpression',
657
    optional: false,
658
    callee: {
659
      type: 'Identifier',
660
      name: encode('eval')
661
    },
662
    arguments: [convertToEstreeExpression(expression) as es.Expression]
663
  }
664
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc