aboutsummaryrefslogtreecommitdiff
path: root/src/runtime.ts
blob: d03e7a960138f6554ff4383e8a34e1a76b0f9234 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { Project, ScriptTarget, Type, Node, StringLiteral, TypeFormatFlags, SyntaxKind } from 'ts-morph'
import path from 'path'
import fs from 'fs'

const project = new Project({
  compilerOptions: {
    target: ScriptTarget.ES3,
  },
})

const typeChecker = project.getTypeChecker()

const sourceFile = project.addSourceFileAtPath(path.resolve("./src/index.ts"))

sourceFile.addStatements('type Foobar = "aaaaaaaa";')

const entryPoint = sourceFile.getExportedDeclarations().get('main')?.[0]

const typeToString = (ty: Type | undefined): string =>
  ty ? typeChecker.compilerObject.typeToString(ty.compilerType) : ''

const getPropertyType = (n: Node, prop: string): Type | undefined => {
  const tt = typeChecker.getTypeAtLocation(n)
  const propSym = tt.getProperty(prop)
  const ty = propSym && typeChecker.getTypeOfSymbolAtLocation(propSym, n)
  return ty
}

const typeRefNode = entryPoint?.getLastChild()

const RESULT_TYPE_NAME = '__$result'

const [statement] = sourceFile.addStatements(`type ${RESULT_TYPE_NAME} = {}`)

const addResult = (name: string, ty: string) => {
  if (statement.isKind(SyntaxKind.TypeAliasDeclaration)) {
    const value = statement.getChildAtIndex(3)
    if (value.isKind(SyntaxKind.TypeLiteral)) {
      value.addProperty({
        name: JSON.stringify(name),
        type: `{ output: ${ty} }`,
      })
    }
  }
}

const accumulateResults = (effTyp: Type, node: Node): string[] => {
  const name = effTyp.getSymbol()?.getName()

  switch (name) {
    case 'ReadFile':
      const [pathTyp] = effTyp.getTypeArguments()
      const filePath = JSON.parse(typeToString(pathTyp))
      const contents = fs.readFileSync(filePath, 'utf-8')
      const hash = Math.random().toFixed(8).slice(2)
      addResult(hash, JSON.stringify(contents))
      return [hash]

    case 'ChainIO':
      const inputTyp = effTyp.getProperty('input')?.getTypeAtLocation(node)
      const inputResults = inputTyp && accumulateResults(inputTyp, node)
      return [...(inputResults ?? [])]
    default:
      return []
  }
}

const evalAccumulator = (effNode: Node, node: Node) => {
  const effTyp = effNode.getType()
  const name = effTyp.getSymbol()?.getName()

  switch (name) {
    case 'PrintString':
    case 'Print':
      console.log(...effTyp.getTypeArguments().map(typeToString));
      return null

    case 'ReadFile':
      const [hash] = accumulateResults(effTyp, node)
      effNode.replaceWithText(`${RESULT_TYPE_NAME}[${JSON.stringify(hash)}]`)
      return null

    case 'WriteFile':
      const [pathTyp, contentsTyp] = effTyp.getTypeArguments()
      const filePath = JSON.parse(typeToString(pathTyp))
      const contents = JSON.parse(typeToString(contentsTyp))
      fs.writeFileSync(filePath, contents)
      return null

    case 'ChainIO':
      const inputTyp = effTyp.getProperty('input')?.getTypeAtLocation(node)
      const chainToKind = effTyp.getProperty('chainTo')?.getTypeAtLocation(node)
      console.log('wow')

      const [hashRes] = inputTyp ? accumulateResults(inputTyp, node) : []
      const chainRes = `(${typeToString(chainToKind)} & { input: ${RESULT_TYPE_NAME}[${JSON.stringify(hashRes)}]['output'] })['return']`

      const updateEffNode = effNode.replaceWithText(chainRes)

      evalAccumulator(updateEffNode, node)
      return null
    default:
      console.log(`${name} effect is unhandled`)
  }

  return null
}

if (typeRefNode) {
  const resultType = entryPoint?.getType()

  if (typeRefNode && entryPoint && resultType?.getSymbol()?.getName() === 'Program') {
    const exitCodeTy = getPropertyType(typeRefNode, 'exitCode')
    const effectTypes = getPropertyType(typeRefNode, 'effects')
    if (effectTypes?.isTuple()) {
      console.log('Effects to run', effectTypes?.getTupleElements().map(eff => typeToString(eff)))

      const effectNodes = entryPoint.getChildrenOfKind(SyntaxKind.TypeReference)
        .flatMap(n => n.getChildrenOfKind(SyntaxKind.TupleType))
        .flatMap(tt => tt.getChildrenOfKind(SyntaxKind.SyntaxList))
        .flatMap(n => n.getChildren())
        .filter(n => !n.isKind(SyntaxKind.CommaToken))

      effectNodes.flatMap(n => evalAccumulator(n, typeRefNode))
    }

    console.log('Exited with code', exitCodeTy?.getLiteralValue())
  } else {
    const ty = typeChecker.getTypeAtLocation(typeRefNode)
    console.log(typeToString(ty))
  }
}

console.log(entryPoint?.print())