-
Notifications
You must be signed in to change notification settings - Fork 0
/
analyzer.js
128 lines (117 loc) · 4.32 KB
/
analyzer.js
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
// The semantic analyzer exports a single function, analyze(match), that
// accepts a grammar match object (the CST) from Ohm and produces the
// internal representation of the program (pretty close to what is usually
// called the AST). This representation also includes entities from the
// standard library, as needed.
import * as core from "./core.js"
class Context {
constructor({ locals = {} }) {
this.locals = new Map(Object.entries(locals))
}
add(name, entity) {
this.locals.set(name, entity)
}
lookup(name) {
return this.locals.get(name)
}
}
export default function analyze(match) {
// Track the context manually via a simple variable. The initial context
// contains the mappings from the standard library. Add to this context
// as necessary. When needing to descent into a new scope, create a new
// context with the current context as its parent. When leaving a scope,
// reset this variable to the parent context.
let context = new Context({ locals: core.standardLibrary })
// The single gate for error checking. Pass in a condition that must be true.
// Use errorLocation to give contextual information about the error that will
// appear: this should be an object whose "at" property is a parse tree node.
// Ohm's getLineAndColumnMessage will be used to prefix the error message.
function must(condition, message, errorLocation) {
if (!condition) {
const prefix = errorLocation.at.source.getLineAndColumnMessage()
throw new Error(`${prefix}${message}`)
}
}
function mustHaveBeenFound(entity, name, at) {
must(entity, `Identifier ${name} not defined`, at)
}
function mustBeAVariable(entity, at) {
must(entity?.kind === "Variable", `Variable expected`, at)
}
function mustBeAFunction(entity, at) {
must(entity?.kind === "Function", `Function expected`, at)
}
function mustBeWritable(variable, at) {
must(variable.writable, `${variable.name} is not writable`, at)
}
function mustHaveCorrectArgumentCount(callee, args, at) {
must(
args.length === callee.paramCount,
`Expected ${callee.paramCount} arg(s), found ${args.length}`,
at
)
}
// The compiler front end analyzes the source code and produces a graph of
// entities (defined in the core module) "rooted" at the Program entity.
const analyzer = match.matcher.grammar.createSemantics().addOperation("rep", {
Program(statements) {
return core.program(statements.rep())
},
Statement_assignment(id, _eq, exp, _semicolon) {
const initializer = exp.rep()
let target = context.lookup(id.sourceString)
if (!target) {
// Not there already, make a new variable and add it
target = core.variable(id.sourceString, true)
context.add(id.sourceString, target)
} else {
// Something was found, whatever it was must be a writable variable
mustBeAVariable(target, { at: id })
mustBeWritable(target, { at: id })
}
return core.assignment(target, initializer)
},
Statement_print(_print, exp, _semicolon) {
return core.print(exp.rep())
},
Exp_binary(exp, op, term) {
return core.binary(op.rep(), exp.rep(), term.rep())
},
Term_binary(term, op, factor) {
return core.binary(op.rep(), term.rep(), factor.rep())
},
Factor_binary(primary, op, factor) {
return core.binary(op.rep(), primary.rep(), factor.rep())
},
Factor_negation(op, primary) {
return core.unary(op.rep(), primary.rep())
},
Primary_parens(_open, exp, _close) {
return exp.rep()
},
Primary_num(num) {
return Number(num.sourceString)
},
Primary_id(id) {
// In Astro, functions never stand alone, so must be a var
const entity = context.lookup(id.sourceString)
mustHaveBeenFound(entity, id.sourceString, { at: id })
mustBeAVariable(entity, { at: id })
return entity
},
Primary_call(id, _open, exps, _close) {
const callee = context.lookup(id.sourceString)
mustBeAFunction(callee, { at: id })
const args = exps.asIteration().rep()
mustHaveCorrectArgumentCount(callee, args, { at: exps })
return core.functionCall(callee, args)
},
_terminal() {
return this.sourceString
},
_iter(...nodes) {
return nodes.map(node => node.rep())
},
})
return analyzer(match).rep()
}