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.
@@ -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
+ }