@defold-typescript/transpiler 0.24.0 → 0.25.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,148 @@
1
+ import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
2
+
3
+ // Every game-object path the project declares, `/`-prefixed and composed the way
4
+ // Defold composes them, plus an honest record of everything the walk could not
5
+ // settle. Shaped like `SceneComponentIndex`: a non-empty `incomplete` means the
6
+ // universe is not provably whole, so no consumer may conclude a path is absent.
7
+ export interface SceneObjectPathIndex {
8
+ readonly paths: ReadonlySet<string>;
9
+ // The documents declaring each path's *leaf* segment, sorted — the files an
10
+ // author would open to rename that object. A composed address is attributed to
11
+ // the collection that names the object, never to the ones that prefixed it.
12
+ readonly declaredIn: ReadonlyMap<string, readonly string[]>;
13
+ readonly incomplete: readonly string[];
14
+ }
15
+
16
+ // The two blocks whose `id` is a leaf segment. `collection_instances` is handled
17
+ // apart, because its id is a namespace rather than an object.
18
+ const LEAF_BLOCKS = ["instances", "embedded_instances"];
19
+
20
+ // Resource paths inside a scene are project-absolute; documents are keyed by
21
+ // project-relative display path, the same convention `resourceKey` follows in
22
+ // `sprite-animation-index.ts`.
23
+ function resourceKey(resource: string): string {
24
+ return resource.startsWith("/") ? resource.slice(1) : resource;
25
+ }
26
+
27
+ function firstField(message: SceneMessage, name: string): string | undefined {
28
+ return (message.fields.get(name) ?? [])[0];
29
+ }
30
+
31
+ function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
32
+ return message.messages.get(name) ?? [];
33
+ }
34
+
35
+ // Build the game-object path universe from already-read scene sources: keys are
36
+ // display paths, values are file text. Pure the same way `buildSceneComponentIndex`
37
+ // is — the filesystem walk belongs to the caller.
38
+ //
39
+ // A `children:` edge is deliberately not a path segment. Defold ids are unique
40
+ // inside one collection and a child object is still addressed `/child`, so
41
+ // parenting is a transform relation; nesting comes from a collection instanced
42
+ // inside another.
43
+ export function buildSceneObjectPathIndex(
44
+ documents: ReadonlyMap<string, string>,
45
+ ): SceneObjectPathIndex {
46
+ const incomplete: string[] = [];
47
+
48
+ if (documents.size === 0) {
49
+ incomplete.push("no scene sources were read, so no game-object path can be proven absent");
50
+ return { paths: new Set(), declaredIn: new Map(), incomplete };
51
+ }
52
+
53
+ const parsed = new Map<string, SceneMessage>();
54
+ for (const [displayPath, text] of documents) {
55
+ try {
56
+ parsed.set(displayPath, parseSceneTextFormat(text));
57
+ } catch (error) {
58
+ if (!(error instanceof SceneTextFormatError)) throw error;
59
+ incomplete.push(`${displayPath}: could not be parsed (${error.message})`);
60
+ }
61
+ }
62
+
63
+ // A collection another document instances is not a world of its own: at
64
+ // runtime its objects only exist under that instance's id, so contributing its
65
+ // own unprefixed paths as well would offer addresses that resolve to nothing.
66
+ const instanced = new Set<string>();
67
+ for (const document of parsed.values()) {
68
+ for (const block of childrenOf(document, "collection_instances")) {
69
+ const collection = firstField(block, "collection");
70
+ if (collection !== undefined) instanced.add(resourceKey(collection));
71
+ }
72
+ }
73
+
74
+ // Memo keyed by display path, so a collection instanced twice is read once;
75
+ // `inProgress` is the cycle guard, without which two collections instancing
76
+ // each other would recur until the stack ran out. Each entry maps a composed
77
+ // path to the document that declared its leaf, so the attribution is carried
78
+ // by the same walk that builds the path rather than recovered afterwards.
79
+ const composed = new Map<string, ReadonlyMap<string, string>>();
80
+ const inProgress = new Set<string>();
81
+
82
+ function pathsOf(displayPath: string): ReadonlyMap<string, string> {
83
+ const done = composed.get(displayPath);
84
+ if (done !== undefined) return done;
85
+ if (inProgress.has(displayPath)) {
86
+ incomplete.push(
87
+ `${displayPath}: is instanced inside itself through a cycle of collection references, so its paths cannot be composed`,
88
+ );
89
+ return new Map();
90
+ }
91
+ const document = parsed.get(displayPath);
92
+ if (document === undefined) return new Map();
93
+
94
+ inProgress.add(displayPath);
95
+ const paths = new Map<string, string>();
96
+
97
+ for (const blockName of LEAF_BLOCKS) {
98
+ for (const block of childrenOf(document, blockName)) {
99
+ const id = firstField(block, "id");
100
+ if (id !== undefined && id !== "") paths.set(`/${id}`, displayPath);
101
+ }
102
+ }
103
+
104
+ for (const block of childrenOf(document, "collection_instances")) {
105
+ const id = firstField(block, "id");
106
+ const collection = firstField(block, "collection");
107
+ if (id === undefined || id === "" || collection === undefined) continue;
108
+ const key = resourceKey(collection);
109
+ if (!parsed.has(key)) {
110
+ incomplete.push(
111
+ `${displayPath}: the collection instance "${id}" names ${collection}, which is not among the project's readable scene documents`,
112
+ );
113
+ continue;
114
+ }
115
+ for (const [nested, declarer] of pathsOf(key)) {
116
+ paths.set(`/${id}${nested}`, declarer);
117
+ }
118
+ }
119
+
120
+ inProgress.delete(displayPath);
121
+ composed.set(displayPath, paths);
122
+ return paths;
123
+ }
124
+
125
+ // Composed for every document, contributed from the un-instanced ones alone: a
126
+ // cycle or a dangling reference reached only from inside an instanced
127
+ // collection is still a gap the caller has to hear about.
128
+ const paths = new Set<string>();
129
+ const declaredIn = new Map<string, string[]>();
130
+ for (const displayPath of parsed.keys()) {
131
+ const own = pathsOf(displayPath);
132
+ if (instanced.has(displayPath)) continue;
133
+ for (const [path, declarer] of own) {
134
+ paths.add(path);
135
+ const declarers = declaredIn.get(path);
136
+ if (declarers === undefined) {
137
+ declaredIn.set(path, [declarer]);
138
+ } else if (!declarers.includes(declarer)) {
139
+ // Two roots instancing one collection under the same id compose the same
140
+ // address from the same leaf, which is one declaring file and not two.
141
+ declarers.push(declarer);
142
+ }
143
+ }
144
+ }
145
+ for (const declarers of declaredIn.values()) declarers.sort();
146
+
147
+ return { paths, declaredIn, incomplete };
148
+ }
@@ -0,0 +1,171 @@
1
+ // Defold's `.go`/`.collection` sources are protobuf text format. A message is a
2
+ // tree of `key: <scalar>` and `key { … }` entries where any key may repeat, so
3
+ // scalars and sub-messages are kept in separate multi-value maps — the format
4
+ // never uses one name as both.
5
+ export interface SceneMessage {
6
+ readonly fields: ReadonlyMap<string, readonly string[]>;
7
+ readonly messages: ReadonlyMap<string, readonly SceneMessage[]>;
8
+ }
9
+
10
+ export class SceneTextFormatError extends Error {
11
+ readonly line: number;
12
+
13
+ constructor(message: string, line: number) {
14
+ super(`${message} (line ${line})`);
15
+ this.name = "SceneTextFormatError";
16
+ this.line = line;
17
+ }
18
+ }
19
+
20
+ const IDENTIFIER_PART = /[A-Za-z0-9_]/;
21
+
22
+ function append<T>(target: Map<string, T[]>, key: string, value: T): void {
23
+ const existing = target.get(key);
24
+ if (existing === undefined) {
25
+ target.set(key, [value]);
26
+ return;
27
+ }
28
+ existing.push(value);
29
+ }
30
+
31
+ // Parses one document. Nothing here knows that `data:` holds a payload: an
32
+ // embedded `.go` is reached by the caller re-invoking this function on the
33
+ // decoded value, which keeps exactly one decode pass per nesting level. A
34
+ // greedy unescape would resolve the deeper level's `\\\"` too and leave the
35
+ // payload unparseable.
36
+ export function parseSceneTextFormat(text: string): SceneMessage {
37
+ let pos = 0;
38
+ let line = 1;
39
+
40
+ function fail(message: string, at: number): never {
41
+ throw new SceneTextFormatError(message, at);
42
+ }
43
+
44
+ function advance(): string {
45
+ const ch = text.charAt(pos);
46
+ pos += 1;
47
+ if (ch === "\n") line += 1;
48
+ return ch;
49
+ }
50
+
51
+ function skipWhitespace(): void {
52
+ while (pos < text.length) {
53
+ const ch = text.charAt(pos);
54
+ if (ch !== " " && ch !== "\t" && ch !== "\r" && ch !== "\n") return;
55
+ advance();
56
+ }
57
+ }
58
+
59
+ function skipInlineWhitespace(): void {
60
+ while (pos < text.length) {
61
+ const ch = text.charAt(pos);
62
+ if (ch !== " " && ch !== "\t") return;
63
+ pos += 1;
64
+ }
65
+ }
66
+
67
+ function readIdentifier(at: number): string {
68
+ const start = pos;
69
+ while (pos < text.length && IDENTIFIER_PART.test(text.charAt(pos))) {
70
+ pos += 1;
71
+ }
72
+ if (pos === start) {
73
+ fail(`unexpected \`${text.charAt(start)}\``, at);
74
+ }
75
+ return text.slice(start, pos);
76
+ }
77
+
78
+ // One or more double-quoted literals, adjacent across whitespace and
79
+ // newlines, concatenated with no separator. `\"`, `\\`, `\n`, `\r` and `\t`
80
+ // resolve; every other backslash pair survives verbatim because it is the
81
+ // next level's escaping.
82
+ function readQuotedScalar(): string {
83
+ let value = "";
84
+ for (;;) {
85
+ const openedAt = line;
86
+ advance();
87
+ let closed = false;
88
+ while (pos < text.length) {
89
+ const ch = advance();
90
+ if (ch === "\\") {
91
+ if (pos >= text.length) break;
92
+ const escaped = advance();
93
+ if (escaped === '"') value += '"';
94
+ else if (escaped === "\\") value += "\\";
95
+ else if (escaped === "n") value += "\n";
96
+ else if (escaped === "r") value += "\r";
97
+ else if (escaped === "t") value += "\t";
98
+ else value += `\\${escaped}`;
99
+ continue;
100
+ }
101
+ if (ch === '"') {
102
+ closed = true;
103
+ break;
104
+ }
105
+ if (ch === "\n") break;
106
+ value += ch;
107
+ }
108
+ if (!closed) {
109
+ fail("unterminated string literal", openedAt);
110
+ }
111
+ const resumePos = pos;
112
+ const resumeLine = line;
113
+ skipWhitespace();
114
+ if (text.charAt(pos) === '"') {
115
+ continue;
116
+ }
117
+ pos = resumePos;
118
+ line = resumeLine;
119
+ return value;
120
+ }
121
+ }
122
+
123
+ function readUnquotedScalar(): string {
124
+ const newline = text.indexOf("\n", pos);
125
+ const end = newline === -1 ? text.length : newline;
126
+ const raw = text.slice(pos, end);
127
+ pos = end;
128
+ return raw.trim();
129
+ }
130
+
131
+ function parseBody(nested: boolean, openedAt: number): SceneMessage {
132
+ const fields = new Map<string, string[]>();
133
+ const messages = new Map<string, SceneMessage[]>();
134
+ for (;;) {
135
+ skipWhitespace();
136
+ if (pos >= text.length) {
137
+ if (nested) {
138
+ fail("unclosed `{`", openedAt);
139
+ }
140
+ break;
141
+ }
142
+ if (text.charAt(pos) === "}") {
143
+ if (!nested) {
144
+ fail("unexpected `}`", line);
145
+ }
146
+ advance();
147
+ break;
148
+ }
149
+ const keyLine = line;
150
+ const key = readIdentifier(keyLine);
151
+ skipInlineWhitespace();
152
+ const delimiter = text.charAt(pos);
153
+ if (delimiter === "{") {
154
+ const braceLine = line;
155
+ advance();
156
+ append(messages, key, parseBody(true, braceLine));
157
+ continue;
158
+ }
159
+ if (delimiter === ":") {
160
+ advance();
161
+ skipInlineWhitespace();
162
+ append(fields, key, text.charAt(pos) === '"' ? readQuotedScalar() : readUnquotedScalar());
163
+ continue;
164
+ }
165
+ fail(`expected \`:\` or \`{\` after \`${key}\``, keyLine);
166
+ }
167
+ return { fields, messages };
168
+ }
169
+
170
+ return parseBody(false, 1);
171
+ }
@@ -0,0 +1,241 @@
1
+ import { parseSceneTextFormat, type SceneMessage, SceneTextFormatError } from "./scene-text-format";
2
+
3
+ // The animation ids each script may play, keyed first by the project-relative
4
+ // resource path of the `.script` that owns them and then by the id of the
5
+ // sprite component they belong to, plus an honest record of every link that
6
+ // could not be settled. A missing key means nothing may be offered for it; an
7
+ // empty set means the component resolved and declares no animation.
8
+ export interface SpriteAnimationIndex {
9
+ readonly byScriptResource: ReadonlyMap<string, ReadonlyMap<string, ReadonlySet<string>>>;
10
+ // The display path of the tile source each keyed component's ids were read
11
+ // from, under the same two keys. Present for exactly the components
12
+ // `byScriptResource` keys, so the two can never disagree about a resource.
13
+ readonly tileSetByScriptResource: ReadonlyMap<string, ReadonlyMap<string, string>>;
14
+ readonly unresolved: readonly string[];
15
+ }
16
+
17
+ // Resource paths inside a scene are project-absolute; the index is keyed the
18
+ // way `buildGuiNodeIndex` keys its own, with the leading `/` dropped, so a
19
+ // caller maps the file it holds forward through the build's output-path math
20
+ // and looks the result up here.
21
+ function resourceKey(resource: string): string {
22
+ return resource.startsWith("/") ? resource.slice(1) : resource;
23
+ }
24
+
25
+ // The keys a caller supplies name files on the host, so they may arrive with
26
+ // native separators; the resource paths they are matched against come out of
27
+ // scene content and are always `/`-separated. Normalizing here enforces that
28
+ // contract at the boundary instead of leaving it to whichever caller happens
29
+ // to build the maps.
30
+ function normalizeDocumentKey(path: string): string {
31
+ return path.replaceAll("\\", "/");
32
+ }
33
+
34
+ const SCRIPT_SUFFIX = ".script";
35
+ const SPRITE_SUFFIX = ".sprite";
36
+
37
+ function firstField(message: SceneMessage, name: string): string | undefined {
38
+ return (message.fields.get(name) ?? [])[0];
39
+ }
40
+
41
+ function childrenOf(message: SceneMessage, name: string): readonly SceneMessage[] {
42
+ return message.messages.get(name) ?? [];
43
+ }
44
+
45
+ // Only `animations { id: … }` counts. Defold may also expose a bare
46
+ // `images { image: … }` entry as a one-frame animation named after the file,
47
+ // which is unverified here — and a wrong suggestion in this slot is a runtime
48
+ // crash rather than a no-op.
49
+ function declaredAnimations(document: SceneMessage): Set<string> {
50
+ const ids = new Set<string>();
51
+ for (const animation of childrenOf(document, "animations")) {
52
+ for (const id of animation.fields.get("id") ?? []) {
53
+ if (id !== "") ids.add(id);
54
+ }
55
+ }
56
+ return ids;
57
+ }
58
+
59
+ interface AssetIndex {
60
+ readonly animationsByTileSet: Map<string, Set<string>>;
61
+ readonly tileSetBySprite: Map<string, string>;
62
+ }
63
+
64
+ function readAssets(assets: ReadonlyMap<string, string>, unresolved: string[]): AssetIndex {
65
+ const animationsByTileSet = new Map<string, Set<string>>();
66
+ const tileSetBySprite = new Map<string, string>();
67
+
68
+ for (const [path, text] of assets) {
69
+ const displayPath = normalizeDocumentKey(path);
70
+ let document: SceneMessage;
71
+ try {
72
+ document = parseSceneTextFormat(text);
73
+ } catch (error) {
74
+ if (!(error instanceof SceneTextFormatError)) throw error;
75
+ unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
76
+ continue;
77
+ }
78
+ if (displayPath.endsWith(SPRITE_SUFFIX)) {
79
+ const tileSet = firstField(document, "tile_set");
80
+ if (tileSet !== undefined) tileSetBySprite.set(displayPath, resourceKey(tileSet));
81
+ continue;
82
+ }
83
+ animationsByTileSet.set(displayPath, declaredAnimations(document));
84
+ }
85
+
86
+ return { animationsByTileSet, tileSetBySprite };
87
+ }
88
+
89
+ // The sprite components of one game object, as `id -> tile_set resource`. An
90
+ // embedded sprite carries its own payload; a referenced one names a `.sprite`
91
+ // document that carries it instead.
92
+ function spriteTileSets(
93
+ object: SceneMessage,
94
+ displayPath: string,
95
+ assets: AssetIndex,
96
+ unresolved: string[],
97
+ ): Map<string, string> {
98
+ const tileSets = new Map<string, string>();
99
+
100
+ for (const embedded of childrenOf(object, "embedded_components")) {
101
+ if (firstField(embedded, "type") !== "sprite") continue;
102
+ const id = firstField(embedded, "id");
103
+ const payload = firstField(embedded, "data");
104
+ if (id === undefined || payload === undefined) continue;
105
+ try {
106
+ const tileSet = firstField(parseSceneTextFormat(payload), "tile_set");
107
+ if (tileSet !== undefined) tileSets.set(id, resourceKey(tileSet));
108
+ } catch (error) {
109
+ if (!(error instanceof SceneTextFormatError)) throw error;
110
+ unresolved.push(
111
+ `${displayPath}: the embedded sprite "${id}" could not be read (${error.message})`,
112
+ );
113
+ }
114
+ }
115
+
116
+ for (const referenced of childrenOf(object, "components")) {
117
+ const component = firstField(referenced, "component");
118
+ const id = firstField(referenced, "id");
119
+ if (id === undefined || component === undefined || !component.endsWith(SPRITE_SUFFIX)) continue;
120
+ const tileSet = assets.tileSetBySprite.get(resourceKey(component));
121
+ if (tileSet === undefined) {
122
+ unresolved.push(
123
+ `${displayPath}: the sprite component "${id}" names ${component}, which is not among the project's sprite documents`,
124
+ );
125
+ continue;
126
+ }
127
+ tileSets.set(id, tileSet);
128
+ }
129
+
130
+ return tileSets;
131
+ }
132
+
133
+ function scriptResourcesOf(object: SceneMessage): string[] {
134
+ const resources: string[] = [];
135
+ for (const component of childrenOf(object, "components")) {
136
+ const resource = firstField(component, "component");
137
+ if (resource === undefined || !resource.endsWith(SCRIPT_SUFFIX)) continue;
138
+ const key = resourceKey(resource);
139
+ if (key !== SCRIPT_SUFFIX.slice(1)) resources.push(key);
140
+ }
141
+ return resources;
142
+ }
143
+
144
+ function isGameObject(message: SceneMessage): boolean {
145
+ return message.messages.has("components") || message.messages.has("embedded_components");
146
+ }
147
+
148
+ export function buildSpriteAnimationIndex(input: {
149
+ scenes: ReadonlyMap<string, string>;
150
+ assets: ReadonlyMap<string, string>;
151
+ }): SpriteAnimationIndex {
152
+ const unresolved: string[] = [];
153
+ const assets = readAssets(input.assets, unresolved);
154
+ const byScriptResource = new Map<string, ReadonlyMap<string, ReadonlySet<string>>>();
155
+ const tileSetByScriptResource = new Map<string, ReadonlyMap<string, string>>();
156
+ const claimedBy = new Map<string, string>();
157
+
158
+ // A `.go` document is a game object at its root; a `.collection` carries one
159
+ // inside each `embedded_instances` `data:` payload, decoded exactly one level
160
+ // at a time. No `instances { prototype: }` edge is followed: the referenced
161
+ // `.go` is its own document and already claims its own script.
162
+ function walk(message: SceneMessage, blockName: string, displayPath: string): void {
163
+ if (isGameObject(message)) {
164
+ claim(message, displayPath);
165
+ }
166
+ if (blockName === "embedded_instances") {
167
+ for (const payload of message.fields.get("data") ?? []) {
168
+ try {
169
+ walk(parseSceneTextFormat(payload), "", displayPath);
170
+ } catch (error) {
171
+ if (!(error instanceof SceneTextFormatError)) throw error;
172
+ unresolved.push(
173
+ `${displayPath}: an embedded game object could not be read (${error.message})`,
174
+ );
175
+ }
176
+ }
177
+ }
178
+ for (const [name, nested] of message.messages) {
179
+ for (const child of nested) {
180
+ walk(child, name, displayPath);
181
+ }
182
+ }
183
+ }
184
+
185
+ function claim(object: SceneMessage, displayPath: string): void {
186
+ const scripts = scriptResourcesOf(object);
187
+ if (scripts.length === 0) return;
188
+
189
+ const tileSets = spriteTileSets(object, displayPath, assets, unresolved);
190
+ const animations = new Map<string, ReadonlySet<string>>();
191
+ const declaringTileSets = new Map<string, string>();
192
+ for (const [id, tileSet] of tileSets) {
193
+ const declared = assets.animationsByTileSet.get(tileSet);
194
+ if (declared === undefined) {
195
+ unresolved.push(
196
+ `${displayPath}: the sprite component "${id}" names the tile source /${tileSet}, which is not among the project's asset documents`,
197
+ );
198
+ continue;
199
+ }
200
+ animations.set(id, declared);
201
+ declaringTileSets.set(id, tileSet);
202
+ }
203
+
204
+ for (const key of scripts) {
205
+ const owner = claimedBy.get(key);
206
+ if (owner !== undefined) {
207
+ byScriptResource.delete(key);
208
+ tileSetByScriptResource.delete(key);
209
+ unresolved.push(
210
+ `${key}: claimed by both ${owner} and ${displayPath}, so its sprite animations are ambiguous`,
211
+ );
212
+ continue;
213
+ }
214
+ claimedBy.set(key, displayPath);
215
+ byScriptResource.set(key, animations);
216
+ tileSetByScriptResource.set(key, declaringTileSets);
217
+ }
218
+ }
219
+
220
+ for (const [path, text] of input.scenes) {
221
+ const displayPath = normalizeDocumentKey(path);
222
+ try {
223
+ walk(parseSceneTextFormat(text), "", displayPath);
224
+ } catch (error) {
225
+ if (!(error instanceof SceneTextFormatError)) throw error;
226
+ unresolved.push(`${displayPath}: could not be parsed (${error.message})`);
227
+ }
228
+ }
229
+
230
+ return { byScriptResource, tileSetByScriptResource, unresolved };
231
+ }
232
+
233
+ // The component id a same-object `"#id"` address names, or `undefined` for
234
+ // every other form. A path form names a component on a *different* game
235
+ // object, so its id must never be resolved against this script's own.
236
+ export function componentIdOfSameObjectAddress(address: string): string | undefined {
237
+ if (!address.startsWith("#")) return undefined;
238
+ const id = address.slice(1);
239
+ if (id === "" || id.includes("/") || id.includes("#")) return undefined;
240
+ return id;
241
+ }
package/src/transpile.ts CHANGED
@@ -158,6 +158,11 @@ function buildAmbientFiles(): Record<string, string> {
158
158
  'export { defineGuiScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
159
159
  "node_modules/@defold-typescript/types/render-script.d.ts":
160
160
  'export { defineRenderScript } from "./src/lifecycle.js";\nexport type { ScriptProperties, ScriptProperty } from "./src/lifecycle.js";\n',
161
+ "node_modules/@defold-typescript/types/editor-script.d.ts": [
162
+ 'export { defineEditorCommand, defineEditorScript } from "./src/editor.js";',
163
+ 'export type { EditorCommandQuery, EditorNode } from "./src/editor.js";',
164
+ "",
165
+ ].join("\n"),
161
166
  };
162
167
  // Seed the Lua 5.1 standard library (math/os/string/table/coroutine + base
163
168
  // globals) so user code can call e.g. `math.randomseed(os.time())`.