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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens, with positions, for the text form of the shader language.
|
|
3
|
+
*
|
|
4
|
+
* Phase A of `Specs/SL_TEXT.md`. Nothing here knows what a shader is: it turns
|
|
5
|
+
* characters into tokens and reports where each one started, so that every
|
|
6
|
+
* later error can say file, line and column. That is the whole reason the
|
|
7
|
+
* position travels on the token rather than being recovered later.
|
|
8
|
+
*
|
|
9
|
+
* HLSL's lexical surface, minus the preprocessor. `#` begins a colour literal
|
|
10
|
+
* and nothing else, which is why a stray `#pragma` gets its own message rather
|
|
11
|
+
* than "unexpected character".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { SLError } from "../ir"
|
|
15
|
+
|
|
16
|
+
export type TokenKind = "ident" | "number" | "hex" | "punct" | "eof"
|
|
17
|
+
|
|
18
|
+
export interface Pos {
|
|
19
|
+
/** 1 based, the way every editor counts. */
|
|
20
|
+
line: number
|
|
21
|
+
/** 1 based. */
|
|
22
|
+
col: number
|
|
23
|
+
/** Offset into the source, for a marker's length. */
|
|
24
|
+
offset: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Token extends Pos {
|
|
28
|
+
kind: TokenKind
|
|
29
|
+
/** The text as written. For a number this is still the source spelling. */
|
|
30
|
+
text: string
|
|
31
|
+
/** Numbers only: the parsed value. */
|
|
32
|
+
value?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An error with a place in a file.
|
|
37
|
+
*
|
|
38
|
+
* Carries the location separately from the message so a caller that is not a
|
|
39
|
+
* terminal (Monaco in the Play editor, esbuild in a build) can put a marker on
|
|
40
|
+
* the right character instead of parsing a string.
|
|
41
|
+
*/
|
|
42
|
+
export class SLParseError extends SLError {
|
|
43
|
+
readonly file: string
|
|
44
|
+
readonly line: number
|
|
45
|
+
readonly column: number
|
|
46
|
+
readonly length: number
|
|
47
|
+
|
|
48
|
+
constructor(message: string, file: string, pos: Pos, length = 1) {
|
|
49
|
+
super(`${file}:${pos.line}:${pos.col}: ${message}`)
|
|
50
|
+
this.name = "SLParseError"
|
|
51
|
+
this.file = file
|
|
52
|
+
this.line = pos.line
|
|
53
|
+
this.column = pos.col
|
|
54
|
+
this.length = length
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Multi character punctuation, longest first.
|
|
60
|
+
*
|
|
61
|
+
* Order is load bearing: `<=` has to be tried before `<`, and `+=` before `+`,
|
|
62
|
+
* or the lexer splits an operator in half and the parser reports something
|
|
63
|
+
* baffling about the second half.
|
|
64
|
+
*/
|
|
65
|
+
const PUNCT = [
|
|
66
|
+
"<=", ">=", "==", "!=", "&&", "||", "++", "--", "+=", "-=", "*=", "/=", "%=",
|
|
67
|
+
"(", ")", "{", "}", "[", "]", ",", ";", ".", "+", "-", "*", "/", "%",
|
|
68
|
+
"<", ">", "!", "?", ":", "=",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
/** Directives that exist in HLSL and deliberately do not exist here. */
|
|
72
|
+
const DIRECTIVES = new Set([
|
|
73
|
+
"pragma", "include", "define", "undef", "ifdef", "ifndef", "endif", "elif", "line",
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
const isDigit = (c: string) => c >= "0" && c <= "9"
|
|
77
|
+
const isHex = (c: string) => isDigit(c) || (c >= "a" && c <= "f") || (c >= "A" && c <= "F")
|
|
78
|
+
const isIdentStart = (c: string) => c === "_" || (c >= "a" && c <= "z") || (c >= "A" && c <= "Z")
|
|
79
|
+
const isIdentPart = (c: string) => isIdentStart(c) || isDigit(c)
|
|
80
|
+
|
|
81
|
+
export function tokenize(source: string, file: string): Token[] {
|
|
82
|
+
const out: Token[] = []
|
|
83
|
+
let i = 0
|
|
84
|
+
let line = 1
|
|
85
|
+
let lineStart = 0
|
|
86
|
+
const here = (): Pos => ({ line, col: i - lineStart + 1, offset: i })
|
|
87
|
+
const fail = (message: string, at: Pos = here(), length = 1): never => {
|
|
88
|
+
throw new SLParseError(message, file, at, length)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
while (i < source.length) {
|
|
92
|
+
const c = source[i]!
|
|
93
|
+
|
|
94
|
+
if (c === "\n") { i++; line++; lineStart = i; continue }
|
|
95
|
+
if (c === " " || c === "\t" || c === "\r") { i++; continue }
|
|
96
|
+
|
|
97
|
+
if (c === "/" && source[i + 1] === "/") {
|
|
98
|
+
while (i < source.length && source[i] !== "\n") i++
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
if (c === "/" && source[i + 1] === "*") {
|
|
102
|
+
const open = here()
|
|
103
|
+
i += 2
|
|
104
|
+
for (;;) {
|
|
105
|
+
if (i >= source.length) fail("this block comment is never closed", open, 2)
|
|
106
|
+
if (source[i] === "*" && source[i + 1] === "/") { i += 2; break }
|
|
107
|
+
if (source[i] === "\n") { line++; lineStart = i + 1 }
|
|
108
|
+
i++
|
|
109
|
+
}
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (c === "#") {
|
|
114
|
+
const start = here()
|
|
115
|
+
let j = i + 1
|
|
116
|
+
while (j < source.length && isIdentPart(source[j]!)) j++
|
|
117
|
+
const body = source.slice(i + 1, j)
|
|
118
|
+
if (DIRECTIVES.has(body)) {
|
|
119
|
+
fail(
|
|
120
|
+
`"#${body}" is a preprocessor directive, and a .sl file has no preprocessor. ` +
|
|
121
|
+
`A file is one fragment function and its declarations; shared code goes in a ` +
|
|
122
|
+
`function, which inlines.`,
|
|
123
|
+
start, body.length + 1,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
if (body.length === 0 || ![...body].every(isHex)) {
|
|
127
|
+
fail(`"#${body}" is not a colour; use #rgb, #rrggbb or #rrggbbaa`, start, body.length + 1)
|
|
128
|
+
}
|
|
129
|
+
out.push({ kind: "hex", text: "#" + body, ...start })
|
|
130
|
+
i = j
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (isDigit(c) || (c === "." && isDigit(source[i + 1] ?? ""))) {
|
|
135
|
+
const start = here()
|
|
136
|
+
let j = i
|
|
137
|
+
while (j < source.length && isDigit(source[j]!)) j++
|
|
138
|
+
if (source[j] === ".") { j++; while (j < source.length && isDigit(source[j]!)) j++ }
|
|
139
|
+
if (source[j] === "e" || source[j] === "E") {
|
|
140
|
+
let k = j + 1
|
|
141
|
+
if (source[k] === "+" || source[k] === "-") k++
|
|
142
|
+
if (isDigit(source[k] ?? "")) { k++; while (k < source.length && isDigit(source[k]!)) k++; j = k }
|
|
143
|
+
}
|
|
144
|
+
const text = source.slice(i, j)
|
|
145
|
+
// HLSL's float suffix. Accepted and dropped: `1.0f` is the same
|
|
146
|
+
// number, and refusing it would only teach the author that this is
|
|
147
|
+
// not quite the language they think it is.
|
|
148
|
+
if (source[j] === "f" || source[j] === "F") {
|
|
149
|
+
if (!isIdentPart(source[j + 1] ?? "")) j++
|
|
150
|
+
}
|
|
151
|
+
const value = Number(text)
|
|
152
|
+
if (!Number.isFinite(value)) fail(`"${text}" is not a number`, start, text.length)
|
|
153
|
+
out.push({ kind: "number", text, value, ...start })
|
|
154
|
+
i = j
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isIdentStart(c)) {
|
|
159
|
+
const start = here()
|
|
160
|
+
let j = i
|
|
161
|
+
while (j < source.length && isIdentPart(source[j]!)) j++
|
|
162
|
+
out.push({ kind: "ident", text: source.slice(i, j), ...start })
|
|
163
|
+
i = j
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const start = here()
|
|
168
|
+
const p = PUNCT.find((op) => source.startsWith(op, i))
|
|
169
|
+
if (p === undefined) fail(`"${c}" means nothing here`, start)
|
|
170
|
+
out.push({ kind: "punct", text: p!, ...start })
|
|
171
|
+
i += p!.length
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
out.push({ kind: "eof", text: "", line, col: i - lineStart + 1, offset: i })
|
|
175
|
+
return out
|
|
176
|
+
}
|