onejs-sl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +266 -0
- package/package.json +60 -0
- package/src/color.ts +16 -0
- package/src/core.ts +24 -0
- package/src/emit/unity.ts +3 -0
- package/src/emit/web.ts +3 -0
- package/src/encode.ts +462 -0
- package/src/hlsl.ts +275 -0
- package/src/index.ts +24 -0
- package/src/ir.ts +301 -0
- package/src/lang/README.md +167 -0
- package/src/lang/ast.ts +87 -0
- package/src/lang/builtins.ts +168 -0
- package/src/lang/check.ts +541 -0
- package/src/lang/index.ts +55 -0
- package/src/lang/lexer.ts +176 -0
- package/src/lang/lower.ts +628 -0
- package/src/lang/parser.ts +460 -0
- package/src/lang/prelude.ts +75 -0
- package/src/limits.ts +50 -0
- package/src/ops.ts +346 -0
- package/src/serial.ts +137 -0
- package/src/shapes.ts +49 -0
- package/src/sl.ts +721 -0
- package/src/tables.ts +15 -0
- package/src/vm.ts +11 -0
- package/src/web.ts +273 -0
- package/src/weblib.ts +1303 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source to AST. A Pratt parser, because expressions are the interesting half.
|
|
3
|
+
*
|
|
4
|
+
* Phase A of `Specs/SL_TEXT.md` section 4. This file decides SHAPE only: what
|
|
5
|
+
* is a declaration, what is a statement, how tightly `*` binds against `+`. It
|
|
6
|
+
* knows nothing about types, about which names exist, or about what any of it
|
|
7
|
+
* lowers to, and it never touches the IR. Everything it refuses, it refuses
|
|
8
|
+
* because the characters cannot be read any other way.
|
|
9
|
+
*
|
|
10
|
+
* HLSL precedence, so that an expression copied out of a `.shader` file means
|
|
11
|
+
* here what it meant there. That is the whole promise of choosing HLSL.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
TYPE_WIDTH,
|
|
16
|
+
type AssignOp, type BinaryOp, type Expr, type FuncDecl, type Param, type Stmt,
|
|
17
|
+
type TextureDecl, type TypeName, type UniformDecl, type Unit,
|
|
18
|
+
} from "./ast"
|
|
19
|
+
import { SLParseError, tokenize, type Pos, type Token } from "./lexer"
|
|
20
|
+
|
|
21
|
+
const TYPE_NAMES = new Set(Object.keys(TYPE_WIDTH))
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Type spellings that exist in HLSL or GLSL and not here, each with the reason.
|
|
25
|
+
*
|
|
26
|
+
* `Specs/SL_TEXT.md` section 2: the IR bounds what the language can say, on
|
|
27
|
+
* purpose. A name in this table gets the reason; a name outside it gets
|
|
28
|
+
* "unknown", and the difference is most of what makes the language learnable.
|
|
29
|
+
*/
|
|
30
|
+
const NOT_A_TYPE: Record<string, string> = {
|
|
31
|
+
vec2: "use float2",
|
|
32
|
+
vec3: "use float3",
|
|
33
|
+
vec4: "use float4",
|
|
34
|
+
int: "there are no integers; a whole number is a float, so write float",
|
|
35
|
+
uint: "there are no integers; a whole number is a float, so write float",
|
|
36
|
+
bool: "there is no bool; a comparison is a float that is 0 or 1",
|
|
37
|
+
half: "use float; the IR has one precision",
|
|
38
|
+
double: "use float; the IR has one precision",
|
|
39
|
+
fixed: "use float; the IR has one precision",
|
|
40
|
+
float2x2: "there are no matrices",
|
|
41
|
+
float3x3: "there are no matrices",
|
|
42
|
+
float4x4: "there are no matrices",
|
|
43
|
+
sampler2D: "use texture2D to declare a texture slot",
|
|
44
|
+
void: "every function returns a value",
|
|
45
|
+
struct: "there are no structs",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Binding power per binary operator, HLSL's. Higher binds tighter. */
|
|
49
|
+
const BINDING: Record<string, number> = {
|
|
50
|
+
"||": 1,
|
|
51
|
+
"&&": 2,
|
|
52
|
+
"==": 3, "!=": 3,
|
|
53
|
+
"<": 4, "<=": 4, ">": 4, ">=": 4,
|
|
54
|
+
"+": 5, "-": 5,
|
|
55
|
+
"*": 6, "/": 6, "%": 6,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ASSIGN_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%="])
|
|
59
|
+
|
|
60
|
+
class Parser {
|
|
61
|
+
private i = 0
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly tokens: Token[],
|
|
65
|
+
private readonly file: string,
|
|
66
|
+
private readonly requireMain: boolean,
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
// MARK: token plumbing
|
|
70
|
+
|
|
71
|
+
private peek(n = 0): Token { return this.tokens[Math.min(this.i + n, this.tokens.length - 1)]! }
|
|
72
|
+
private next(): Token { return this.tokens[this.i++]! }
|
|
73
|
+
private at(text: string): boolean { const t = this.peek(); return t.kind !== "eof" && t.text === text }
|
|
74
|
+
private eat(text: string): boolean { if (this.at(text)) { this.i++; return true } return false }
|
|
75
|
+
|
|
76
|
+
private fail(message: string, at: Token = this.peek()): never {
|
|
77
|
+
throw new SLParseError(message, this.file, at as Pos, Math.max(1, at.text.length))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private expect(text: string, what: string): Token {
|
|
81
|
+
if (!this.at(text)) {
|
|
82
|
+
const got = this.peek()
|
|
83
|
+
this.fail(`expected "${text}" ${what}, got ${describe(got)}`, got)
|
|
84
|
+
}
|
|
85
|
+
return this.next()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private expectIdent(what: string): Token {
|
|
89
|
+
const t = this.peek()
|
|
90
|
+
if (t.kind !== "ident") this.fail(`expected ${what}, got ${describe(t)}`, t)
|
|
91
|
+
return this.next()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private expectType(what: string): TypeName {
|
|
95
|
+
const t = this.expectIdent(what)
|
|
96
|
+
if (TYPE_NAMES.has(t.text)) return t.text as TypeName
|
|
97
|
+
const why = NOT_A_TYPE[t.text]
|
|
98
|
+
if (why !== undefined) this.fail(`"${t.text}" is not a type here: ${why}`, t)
|
|
99
|
+
this.fail(`"${t.text}" is not a type; the types are float, float2, float3 and float4`, t)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// MARK: the file
|
|
103
|
+
|
|
104
|
+
parseUnit(): Unit {
|
|
105
|
+
const unit: Unit = {
|
|
106
|
+
file: this.file, uniforms: [], textures: [], consts: [], funcs: [], main: null,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
while (this.peek().kind !== "eof") {
|
|
110
|
+
const t = this.peek()
|
|
111
|
+
|
|
112
|
+
if (t.text === "uniform") { unit.uniforms.push(this.parseUniform()); continue }
|
|
113
|
+
if (t.text === "texture2D") { unit.textures.push(this.parseTexture()); continue }
|
|
114
|
+
if (t.text === "const") {
|
|
115
|
+
const c = this.parseConst()
|
|
116
|
+
unit.consts.push(c)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (t.kind === "ident") {
|
|
120
|
+
const fn = this.parseFunction()
|
|
121
|
+
if (fn.name === "main") {
|
|
122
|
+
if (unit.main !== null) {
|
|
123
|
+
this.fail("this file already declares main; a .sl file is exactly one fragment function", t)
|
|
124
|
+
}
|
|
125
|
+
unit.main = fn
|
|
126
|
+
} else {
|
|
127
|
+
unit.funcs.push(fn)
|
|
128
|
+
}
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
this.fail(
|
|
132
|
+
`expected a declaration (uniform, texture2D, const, or a function), got ${describe(t)}`, t,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (unit.main === null && this.requireMain) {
|
|
137
|
+
this.fail(
|
|
138
|
+
"this file declares no main. A .sl file is one fragment function: add " +
|
|
139
|
+
"`float4 main() { ... }`",
|
|
140
|
+
this.peek(),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
return unit
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private parseUniform(): UniformDecl {
|
|
147
|
+
const kw = this.next()
|
|
148
|
+
const type = this.expectType("a type after uniform")
|
|
149
|
+
const name = this.expectIdent("a uniform name")
|
|
150
|
+
let init: Expr | null = null
|
|
151
|
+
if (this.eat("=")) init = this.parseExpr()
|
|
152
|
+
this.expect(";", "after a uniform declaration")
|
|
153
|
+
return { name: name.text, type, init, pos: kw }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private parseTexture(): TextureDecl {
|
|
157
|
+
const kw = this.next()
|
|
158
|
+
const name = this.expectIdent("a texture name")
|
|
159
|
+
if (this.at("=")) {
|
|
160
|
+
this.fail(
|
|
161
|
+
"a texture has no default; the host binds it by name through the textures prop",
|
|
162
|
+
this.peek(),
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
this.expect(";", "after a texture declaration")
|
|
166
|
+
return { name: name.text, pos: kw }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private parseConst(): Extract<Stmt, { k: "const" }> {
|
|
170
|
+
const kw = this.next()
|
|
171
|
+
const type = this.expectType("a type after const")
|
|
172
|
+
const name = this.expectIdent("a name")
|
|
173
|
+
this.expect("=", "after a const's name; a const has to have a value")
|
|
174
|
+
const init = this.parseExpr()
|
|
175
|
+
this.expect(";", "after a const")
|
|
176
|
+
return { k: "const", type, name: name.text, init, pos: kw }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private parseFunction(): FuncDecl {
|
|
180
|
+
const start = this.peek()
|
|
181
|
+
const ret = this.expectType("a return type")
|
|
182
|
+
const name = this.expectIdent("a function name")
|
|
183
|
+
if (!this.at("(")) {
|
|
184
|
+
this.fail(
|
|
185
|
+
`expected "(" after ${name.text}. Only uniforms, textures, consts and functions live ` +
|
|
186
|
+
`at the top level of a file; a value belongs inside main or a function`,
|
|
187
|
+
this.peek(),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
this.next()
|
|
191
|
+
const params: Param[] = []
|
|
192
|
+
if (!this.at(")")) {
|
|
193
|
+
for (;;) {
|
|
194
|
+
const p = this.peek()
|
|
195
|
+
const ptype = this.expectType("a parameter type")
|
|
196
|
+
const pname = this.expectIdent("a parameter name")
|
|
197
|
+
params.push({ type: ptype, name: pname.text, pos: p })
|
|
198
|
+
if (!this.eat(",")) break
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.expect(")", "after the parameters")
|
|
202
|
+
const body = this.parseBlock()
|
|
203
|
+
return { name: name.text, ret, params, body, pos: start, prelude: false }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// MARK: statements
|
|
207
|
+
|
|
208
|
+
private parseBlock(): Stmt[] {
|
|
209
|
+
this.expect("{", "to open a body")
|
|
210
|
+
const out: Stmt[] = []
|
|
211
|
+
while (!this.at("}")) {
|
|
212
|
+
if (this.peek().kind === "eof") this.fail("this body is never closed", this.peek())
|
|
213
|
+
out.push(this.parseStmt())
|
|
214
|
+
}
|
|
215
|
+
this.next()
|
|
216
|
+
return out
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A braced block, or the single statement HLSL lets an `if` or `for` carry. */
|
|
220
|
+
private parseBody(): Stmt[] {
|
|
221
|
+
if (this.at("{")) return this.parseBlock()
|
|
222
|
+
return [this.parseStmt()]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private parseStmt(): Stmt {
|
|
226
|
+
const t = this.peek()
|
|
227
|
+
|
|
228
|
+
if (t.text === "const") return this.parseConst()
|
|
229
|
+
if (t.text === "if") return this.parseIf()
|
|
230
|
+
if (t.text === "for") return this.parseFor()
|
|
231
|
+
if (t.text === "while" || t.text === "do") {
|
|
232
|
+
this.fail(
|
|
233
|
+
`there is no ${t.text} loop: the VM has no branches, so a loop has to unroll, and ` +
|
|
234
|
+
`only a for loop with constant bounds can. See a for loop`,
|
|
235
|
+
t,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
if (t.text === "discard") {
|
|
239
|
+
this.fail("there is no discard; return a colour with alpha 0 instead", t)
|
|
240
|
+
}
|
|
241
|
+
if (t.text === "return") {
|
|
242
|
+
this.next()
|
|
243
|
+
const value = this.parseExpr()
|
|
244
|
+
this.expect(";", "after a return")
|
|
245
|
+
return { k: "return", value, pos: t }
|
|
246
|
+
}
|
|
247
|
+
if (t.text === "uniform" || t.text === "texture2D") {
|
|
248
|
+
this.fail(`a ${t.text} is declared at the top level of the file, not inside a body`, t)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// A declaration is a type followed by a name. Anything else starting
|
|
252
|
+
// with an identifier is an expression, and the only expression a
|
|
253
|
+
// statement may be is the left of an assignment.
|
|
254
|
+
if (t.kind === "ident" && this.peek(1).kind === "ident") {
|
|
255
|
+
const type = this.expectType("a type")
|
|
256
|
+
const name = this.expectIdent("a name")
|
|
257
|
+
if (this.at("(")) {
|
|
258
|
+
this.fail(
|
|
259
|
+
"a function is declared at the top level of the file, not inside another function",
|
|
260
|
+
this.peek(),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
this.expect("=", `after ${name.text}; every local is declared with a value`)
|
|
264
|
+
const init = this.parseExpr()
|
|
265
|
+
this.expect(";", "after a declaration")
|
|
266
|
+
return { k: "var", type, name: name.text, init, pos: t }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const target = this.parseExpr()
|
|
270
|
+
const op = this.peek()
|
|
271
|
+
if (op.kind === "punct" && ASSIGN_OPS.has(op.text)) {
|
|
272
|
+
this.next()
|
|
273
|
+
const value = this.parseExpr()
|
|
274
|
+
this.expect(";", "after an assignment")
|
|
275
|
+
return { k: "assign", target, op: op.text as AssignOp, value, pos: t }
|
|
276
|
+
}
|
|
277
|
+
if (op.text === "++" || op.text === "--") {
|
|
278
|
+
this.fail(`${op.text} is only a for loop's update; write \`x = x + 1;\``, op)
|
|
279
|
+
}
|
|
280
|
+
this.fail(
|
|
281
|
+
"this statement has no effect. A statement is a declaration, an assignment, an if, a " +
|
|
282
|
+
"for or a return",
|
|
283
|
+
t,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private parseIf(): Stmt {
|
|
288
|
+
const kw = this.next()
|
|
289
|
+
this.expect("(", "after if")
|
|
290
|
+
const cond = this.parseExpr()
|
|
291
|
+
this.expect(")", "after an if condition")
|
|
292
|
+
const then = this.parseBody()
|
|
293
|
+
let otherwise: Stmt[] = []
|
|
294
|
+
if (this.at("else")) {
|
|
295
|
+
this.next()
|
|
296
|
+
otherwise = this.at("if") ? [this.parseIf()] : this.parseBody()
|
|
297
|
+
}
|
|
298
|
+
return { k: "if", cond, then, else: otherwise, pos: kw }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* `for (int i = A; i < B; i++)`, and nothing more exotic than `i += K`.
|
|
303
|
+
*
|
|
304
|
+
* The shape is narrow because the loop UNROLLS: the counter is a compile
|
|
305
|
+
* time number, not a value, and every form this refuses is one where it
|
|
306
|
+
* could not be. Section 3.6 of the spec, and the error says so rather than
|
|
307
|
+
* leaving an author to guess which part offended.
|
|
308
|
+
*/
|
|
309
|
+
private parseFor(): Stmt {
|
|
310
|
+
const kw = this.next()
|
|
311
|
+
this.expect("(", "after for")
|
|
312
|
+
|
|
313
|
+
const decl = this.peek()
|
|
314
|
+
if (decl.text !== "int" && decl.text !== "float") {
|
|
315
|
+
this.fail(
|
|
316
|
+
`a for loop starts by declaring its counter, as in \`for (int i = 0; i < 4; i++)\`, ` +
|
|
317
|
+
`got ${describe(decl)}`, decl,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
this.next()
|
|
321
|
+
const counter = this.expectIdent("a counter name")
|
|
322
|
+
this.expect("=", "after the counter")
|
|
323
|
+
const from = this.parseExpr()
|
|
324
|
+
this.expect(";", "after the counter's start")
|
|
325
|
+
|
|
326
|
+
const lhs = this.expectIdent("the counter in the loop's condition")
|
|
327
|
+
if (lhs.text !== counter.text) {
|
|
328
|
+
this.fail(`this loop counts ${counter.text}, so its condition has to test ${counter.text}`, lhs)
|
|
329
|
+
}
|
|
330
|
+
const cmp = this.peek()
|
|
331
|
+
if (cmp.text !== "<" && cmp.text !== "<=") {
|
|
332
|
+
this.fail(
|
|
333
|
+
`a for loop counts up, so its condition is \`${counter.text} <\` or \`${counter.text} <=\`, ` +
|
|
334
|
+
`got ${describe(cmp)}`, cmp,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
this.next()
|
|
338
|
+
const to = this.parseExpr()
|
|
339
|
+
this.expect(";", "after the loop's bound")
|
|
340
|
+
|
|
341
|
+
const up = this.expectIdent("the counter in the loop's update")
|
|
342
|
+
if (up.text !== counter.text) {
|
|
343
|
+
this.fail(`this loop counts ${counter.text}, so its update has to change ${counter.text}`, up)
|
|
344
|
+
}
|
|
345
|
+
let step: Expr = { k: "num", value: 1, pos: up }
|
|
346
|
+
if (this.eat("++")) {
|
|
347
|
+
// nothing: the step is 1
|
|
348
|
+
} else if (this.eat("+=")) {
|
|
349
|
+
step = this.parseExpr()
|
|
350
|
+
} else {
|
|
351
|
+
this.fail(
|
|
352
|
+
`a for loop's update is \`${counter.text}++\` or \`${counter.text} += n\`, got ` +
|
|
353
|
+
`${describe(this.peek())}`, this.peek(),
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
this.expect(")", "after the loop's update")
|
|
357
|
+
const body = this.parseBody()
|
|
358
|
+
return { k: "for", counter: counter.text, from, to, inclusive: cmp.text === "<=", step, body, pos: kw }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// MARK: expressions
|
|
362
|
+
|
|
363
|
+
parseExpr(): Expr { return this.parseBinary(0) }
|
|
364
|
+
|
|
365
|
+
private parseBinary(min: number): Expr {
|
|
366
|
+
let left = this.parseUnary()
|
|
367
|
+
for (;;) {
|
|
368
|
+
const t = this.peek()
|
|
369
|
+
if (t.kind !== "punct") break
|
|
370
|
+
const bp = BINDING[t.text]
|
|
371
|
+
if (bp === undefined || bp < min) break
|
|
372
|
+
this.next()
|
|
373
|
+
const right = this.parseBinary(bp + 1)
|
|
374
|
+
left = { k: "binary", op: t.text as BinaryOp, a: left, b: right, pos: t }
|
|
375
|
+
}
|
|
376
|
+
// The conditional binds looser than every binary operator and is right
|
|
377
|
+
// associative, so `a ? b : c ? d : e` groups to the right, as in HLSL.
|
|
378
|
+
if (min === 0 && this.at("?")) {
|
|
379
|
+
const q = this.next()
|
|
380
|
+
const then = this.parseExpr()
|
|
381
|
+
this.expect(":", "in a ?: conditional")
|
|
382
|
+
const otherwise = this.parseExpr()
|
|
383
|
+
return { k: "cond", cond: left, then, else: otherwise, pos: q }
|
|
384
|
+
}
|
|
385
|
+
return left
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private parseUnary(): Expr {
|
|
389
|
+
const t = this.peek()
|
|
390
|
+
if (t.kind === "punct" && (t.text === "-" || t.text === "+" || t.text === "!")) {
|
|
391
|
+
this.next()
|
|
392
|
+
return { k: "unary", op: t.text, arg: this.parseUnary(), pos: t }
|
|
393
|
+
}
|
|
394
|
+
if (t.text === "++" || t.text === "--") {
|
|
395
|
+
this.fail(`${t.text} is only a for loop's update`, t)
|
|
396
|
+
}
|
|
397
|
+
return this.parsePostfix(this.parsePrimary())
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private parsePostfix(expr: Expr): Expr {
|
|
401
|
+
for (;;) {
|
|
402
|
+
if (this.at(".")) {
|
|
403
|
+
this.next()
|
|
404
|
+
const name = this.expectIdent("a component or a shape name after \".\"")
|
|
405
|
+
expr = { k: "member", obj: expr, name: name.text, pos: name }
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
if (this.at("(")) {
|
|
409
|
+
const open = this.next()
|
|
410
|
+
const args: Expr[] = []
|
|
411
|
+
if (!this.at(")")) {
|
|
412
|
+
for (;;) {
|
|
413
|
+
args.push(this.parseExpr())
|
|
414
|
+
if (!this.eat(",")) break
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
this.expect(")", "after the arguments")
|
|
418
|
+
expr = { k: "call", callee: expr, args, pos: open }
|
|
419
|
+
continue
|
|
420
|
+
}
|
|
421
|
+
if (this.at("[")) {
|
|
422
|
+
this.fail("there are no arrays; index a component with .x, .y, .z or .w", this.peek())
|
|
423
|
+
}
|
|
424
|
+
return expr
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private parsePrimary(): Expr {
|
|
429
|
+
const t = this.next()
|
|
430
|
+
if (t.kind === "number") return { k: "num", value: t.value!, pos: t }
|
|
431
|
+
if (t.kind === "hex") return { k: "hex", hex: t.text, pos: t }
|
|
432
|
+
if (t.kind === "ident") return { k: "ident", name: t.text, pos: t }
|
|
433
|
+
if (t.text === "(") {
|
|
434
|
+
const inner = this.parseExpr()
|
|
435
|
+
this.expect(")", "to close a group")
|
|
436
|
+
return inner
|
|
437
|
+
}
|
|
438
|
+
this.fail(`expected a value, got ${describe(t)}`, t)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function describe(t: Token): string {
|
|
443
|
+
if (t.kind === "eof") return "the end of the file"
|
|
444
|
+
return `"${t.text}"`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export interface ParseOptions {
|
|
448
|
+
file?: string
|
|
449
|
+
/**
|
|
450
|
+
* Off for the prelude, which is a library of functions and has no main.
|
|
451
|
+
* Nothing else should turn it off: a `.sl` file without a main renders
|
|
452
|
+
* nothing, and finding that out at parse time is the point.
|
|
453
|
+
*/
|
|
454
|
+
requireMain?: boolean
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function parseUnit(source: string, options: ParseOptions = {}): Unit {
|
|
458
|
+
const file = options.file ?? "program.sl"
|
|
459
|
+
return new Parser(tokenize(source, file), file, options.requireMain ?? true).parseUnit()
|
|
460
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standard library, written in the language itself.
|
|
3
|
+
*
|
|
4
|
+
* `Specs/SL_TEXT.md` 3.8: the things every shader pastes from somewhere, as
|
|
5
|
+
* ordinary functions that inline, available in every file without an import. A
|
|
6
|
+
* function declared in a file shadows a prelude function of the same name.
|
|
7
|
+
*
|
|
8
|
+
* ADDING TO IT COSTS A FUNCTION AND A TEST, NOT AN OPCODE IN THREE PLACES. That
|
|
9
|
+
* is the whole argument for having a text language with inlining functions: a
|
|
10
|
+
* library entry is source, so it lands in the VM, in generated HLSL and in the
|
|
11
|
+
* hash by being ordinary code, and neither backend learns anything new.
|
|
12
|
+
*
|
|
13
|
+
* WHY THE SOURCE IS A STRING AND NOT A FILE. `onejs-sl` ships raw TypeScript
|
|
14
|
+
* and runs inside a browser worker on play.onejs.com, where
|
|
15
|
+
* there is no filesystem to read a `.sl` from, and the esbuild loader that
|
|
16
|
+
* would inline one does not exist until Phase B. A template literal parses to
|
|
17
|
+
* exactly the same unit, highlights the same in an editor, and needs nothing
|
|
18
|
+
* from the bundler.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { FuncDecl } from "./ast"
|
|
22
|
+
import { check } from "./check"
|
|
23
|
+
import { parseUnit } from "./parser"
|
|
24
|
+
|
|
25
|
+
export const PRELUDE_SOURCE = `
|
|
26
|
+
// Rotate a point about the origin. Translate first if you want another centre.
|
|
27
|
+
float2 rotate(float2 p, float angle) {
|
|
28
|
+
float c = cos(angle);
|
|
29
|
+
float s = sin(angle);
|
|
30
|
+
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Cartesian to polar: x is the angle in radians, y is the radius.
|
|
34
|
+
float2 polar(float2 p) {
|
|
35
|
+
return float2(atan2(p.y, p.x), length(p));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Signed distance to a circle at the origin. Negative inside.
|
|
39
|
+
float circle(float2 p, float r) {
|
|
40
|
+
return length(p) - r;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Signed distance to an axis aligned box of the given half extents.
|
|
44
|
+
float box(float2 p, float2 size) {
|
|
45
|
+
float2 d = abs(p) - size;
|
|
46
|
+
return length(max(d, 0)) + min(max(d.x, d.y), 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Repeat a unit cell n times across the input, centred on zero in each cell.
|
|
50
|
+
float2 tile(float2 p, float n) {
|
|
51
|
+
return frac(p * n) - 0.5;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Inigo Quilez's cosine palette. Four float3s in, a colour out, no texture.
|
|
55
|
+
float3 palette(float t, float3 a, float3 b, float3 c, float3 d) {
|
|
56
|
+
return a + b * cos(6.28318530718 * (c * t + d));
|
|
57
|
+
}
|
|
58
|
+
`
|
|
59
|
+
|
|
60
|
+
let cached: FuncDecl[] | null = null
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The prelude's functions, parsed and checked once.
|
|
64
|
+
*
|
|
65
|
+
* Checked against a unit with no uniforms and no textures of its own, which is
|
|
66
|
+
* what it is: a parameter here called `p` must not collide with a uniform in
|
|
67
|
+
* whichever file happens to use it.
|
|
68
|
+
*/
|
|
69
|
+
export function preludeFunctions(): FuncDecl[] {
|
|
70
|
+
if (cached !== null) return cached
|
|
71
|
+
const unit = parseUnit(PRELUDE_SOURCE, { file: "prelude.sl", requireMain: false })
|
|
72
|
+
check(unit, [], { requireMain: false })
|
|
73
|
+
cached = unit.funcs.map((fn) => ({ ...fn, prelude: true }))
|
|
74
|
+
return cached
|
|
75
|
+
}
|
package/src/limits.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `onejs-sl/limits`: whether a program fits the VM, without encoding it.
|
|
3
|
+
*
|
|
4
|
+
* The VM is the one backend with limits. A compiled backend (every web build,
|
|
5
|
+
* an editor with a generated shader) runs any program; the VM runs one that is
|
|
6
|
+
* at most `MAX_INSTRUCTIONS` long and never needs more than `REGISTERS`
|
|
7
|
+
* values at once. OneJS refuses an over-limit program only in a build that
|
|
8
|
+
* still runs the VM. A host that wants to warn an author ahead of that asks
|
|
9
|
+
* here, and gets the same answer `encode` would give, because both run the
|
|
10
|
+
* same checks.
|
|
11
|
+
*/
|
|
12
|
+
import { SLError, type Program } from "./ir"
|
|
13
|
+
import { allocate, checkBudget, forVm, reachable, wireOf } from "./encode"
|
|
14
|
+
|
|
15
|
+
export { REGISTERS, MAX_INSTRUCTIONS } from "./encode"
|
|
16
|
+
export { VM_UNIFORMS, VM_TEXTURES } from "./ops"
|
|
17
|
+
|
|
18
|
+
export interface VmFit {
|
|
19
|
+
/** Every OneJS VM at `wire` or newer runs it. */
|
|
20
|
+
fits: boolean
|
|
21
|
+
/** Operations after dead code is dropped, as the VM would run them. */
|
|
22
|
+
instructions: number
|
|
23
|
+
/** The most values live at once, when the register file held them all. */
|
|
24
|
+
registers?: number
|
|
25
|
+
/** The oldest VM encoding that can run it. */
|
|
26
|
+
wire: number
|
|
27
|
+
/** Why it does not fit, in the words `encode` throws with. */
|
|
28
|
+
reason?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function vmFit(source: Program): VmFit {
|
|
32
|
+
const program = forVm(source)
|
|
33
|
+
const order = reachable(program.nodes, program.result)
|
|
34
|
+
const reasons: string[] = []
|
|
35
|
+
let registers: number | undefined
|
|
36
|
+
const attempt = (check: () => void) => {
|
|
37
|
+
try {
|
|
38
|
+
check()
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (!(e instanceof SLError)) throw e
|
|
41
|
+
reasons.push(e.message)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
attempt(() => checkBudget(program, order))
|
|
45
|
+
attempt(() => { registers = allocate(program, order).peak })
|
|
46
|
+
const fit: VmFit = { fits: reasons.length === 0, instructions: order.length, wire: wireOf(source, program) }
|
|
47
|
+
if (registers !== undefined) fit.registers = registers
|
|
48
|
+
if (reasons.length > 0) fit.reason = reasons.join(" ")
|
|
49
|
+
return fit
|
|
50
|
+
}
|