Skip to content

Commit 3cb6a5d

Browse files
Better diagnostics
1 parent d50079a commit 3cb6a5d

File tree

7 files changed

+135
-62
lines changed

7 files changed

+135
-62
lines changed

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ struct PackagingPlanner {
278278
) {
279279
var content = try String(contentsOf: inputPath, encoding: .utf8)
280280
let options = PreprocessOptions(substitutions: substitutions)
281-
content = try preprocess(source: content, options: options)
281+
content = try preprocess(source: content, file: file, options: options)
282282
try content.write(toFile: $0.output, atomically: true, encoding: .utf8)
283283
}
284284
}

Plugins/PackageToJS/Sources/Preprocess.swift

Lines changed: 107 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
/// `@VARIABLE@` will be substituted with the value of the variable.
1717
///
1818
/// The preprocessor will return the preprocessed source code.
19-
func preprocess(source: String, options: PreprocessOptions) throws -> String {
20-
let tokens = try Preprocessor.tokenize(source: source)
21-
let parsed = try Preprocessor.parse(tokens: tokens, source: source, options: options)
22-
return try Preprocessor.preprocess(parsed: parsed, source: source, options: options)
19+
func preprocess(source: String, file: String?, options: PreprocessOptions) throws -> String {
20+
let preprocessor = Preprocessor(source: source, file: file, options: options)
21+
let tokens = try preprocessor.tokenize()
22+
let parsed = try preprocessor.parse(tokens: tokens)
23+
return try preprocessor.preprocess(parsed: parsed)
2324
}
2425

2526
struct PreprocessOptions {
@@ -42,61 +43,117 @@ private struct Preprocessor {
4243
let position: String.Index
4344
}
4445

45-
struct PreprocessorError: Error {
46+
struct PreprocessorError: Error, CustomStringConvertible {
47+
let file: String?
4648
let message: String
4749
let source: String
4850
let line: Int
4951
let column: Int
5052

51-
init(message: String, source: String, line: Int, column: Int) {
53+
init(file: String?, message: String, source: String, line: Int, column: Int) {
54+
self.file = file
5255
self.message = message
5356
self.source = source
5457
self.line = line
5558
self.column = column
5659
}
5760

58-
init(message: String, source: String, index: String.Index) {
59-
func consumeLineColumn(from index: String.Index, in source: String) -> (Int, Int) {
60-
var line = 1
61-
var column = 1
62-
for char in source[..<index] {
63-
if char == "\n" {
64-
line += 1
65-
column = 1
66-
} else {
67-
column += 1
68-
}
61+
init(file: String?, message: String, source: String, index: String.Index) {
62+
let (line, column) = Self.computeLineAndColumn(from: index, in: source)
63+
self.init(file: file, message: message, source: source, line: line, column: column)
64+
}
65+
66+
/// Get the 1-indexed line and column
67+
private static func computeLineAndColumn(from index: String.Index, in source: String) -> (line: Int, column: Int) {
68+
var line = 1
69+
var column = 1
70+
for char in source[..<index] {
71+
if char == "\n" {
72+
line += 1
73+
column = 1
74+
} else {
75+
column += 1
6976
}
70-
return (line, column)
7177
}
72-
self.message = message
73-
self.source = source
74-
let (line, column) = consumeLineColumn(from: index, in: source)
75-
self.line = line
76-
self.column = column
78+
return (line, column)
7779
}
7880

79-
static func expected(
80-
_ expected: CustomStringConvertible, at index: String.Index, in source: String
81-
) -> PreprocessorError {
82-
return PreprocessorError(
83-
message: "Expected \(expected) at \(index)", source: source, index: index)
81+
var description: String {
82+
let lines = source.split(separator: "\n", omittingEmptySubsequences: false)
83+
let lineIndex = line - 1
84+
let lineNumberWidth = "\(line + 1)".count
85+
86+
var description = ""
87+
if let file = file {
88+
description += "\(file):"
89+
}
90+
description += "\(line):\(column): \(message)\n"
91+
92+
// Show context lines
93+
if lineIndex > 0 {
94+
description += formatLine(number: line - 1, content: lines[lineIndex - 1], width: lineNumberWidth)
95+
}
96+
description += formatLine(number: line, content: lines[lineIndex], width: lineNumberWidth)
97+
description += formatPointer(column: column, width: lineNumberWidth)
98+
if lineIndex + 1 < lines.count {
99+
description += formatLine(number: line + 1, content: lines[lineIndex + 1], width: lineNumberWidth)
100+
}
101+
102+
return description
84103
}
85104

86-
static func unexpected(token: Token, at index: String.Index, in source: String)
87-
-> PreprocessorError
88-
{
89-
return PreprocessorError(
90-
message: "Unexpected token \(token) at \(index)", source: source, index: index)
105+
private func formatLine(number: Int, content: String.SubSequence, width: Int) -> String {
106+
return "\(number)".padding(toLength: width, withPad: " ", startingAt: 0) + " | \(content)\n"
91107
}
92108

93-
static func eof(at index: String.Index, in source: String) -> PreprocessorError {
94-
return PreprocessorError(
95-
message: "Unexpected end of input", source: source, index: index)
109+
private func formatPointer(column: Int, width: Int) -> String {
110+
let padding = String(repeating: " ", count: width) + " | " + String(repeating: " ", count: column - 1)
111+
return padding + "^\n"
96112
}
97113
}
98114

99-
static func tokenize(source: String) throws -> [TokenInfo] {
115+
let source: String
116+
let file: String?
117+
let options: PreprocessOptions
118+
119+
init(source: String, file: String?, options: PreprocessOptions) {
120+
self.source = source
121+
self.file = file
122+
self.options = options
123+
}
124+
125+
func unexpectedTokenError(expected: Token?, token: Token, at index: String.Index) -> PreprocessorError {
126+
let message = expected.map { "Expected \($0) but got \(token)" } ?? "Unexpected token \(token)"
127+
return PreprocessorError(
128+
file: file,
129+
message: message, source: source, index: index)
130+
}
131+
132+
func unexpectedCharacterError(expected: CustomStringConvertible, character: Character, at index: String.Index) -> PreprocessorError {
133+
return PreprocessorError(
134+
file: file,
135+
message: "Expected \(expected) but got \(character)", source: source, index: index)
136+
}
137+
138+
func unexpectedDirectiveError(at index: String.Index) -> PreprocessorError {
139+
return PreprocessorError(
140+
file: file,
141+
message: "Unexpected directive", source: source, index: index)
142+
}
143+
144+
func eofError(at index: String.Index) -> PreprocessorError {
145+
return PreprocessorError(
146+
file: file,
147+
message: "Unexpected end of input", source: source, index: index)
148+
}
149+
150+
func undefinedVariableError(name: String, at index: String.Index) -> PreprocessorError {
151+
return PreprocessorError(
152+
file: file,
153+
message: "Undefined variable \(name)", source: source, index: index)
154+
}
155+
156+
func tokenize() throws -> [TokenInfo] {
100157
var cursor = source.startIndex
101158
var tokens: [TokenInfo] = []
102159

@@ -121,7 +178,7 @@ private struct Preprocessor {
121178

122179
func expect(_ expected: Character) throws {
123180
guard try peek() == expected else {
124-
throw PreprocessorError.expected(expected, at: cursor, in: source)
181+
throw unexpectedCharacterError(expected: expected, character: try peek(), at: cursor)
125182
}
126183
consume()
127184
}
@@ -131,24 +188,24 @@ private struct Preprocessor {
131188
let endIndex = source.index(
132189
cursor, offsetBy: expected.count, limitedBy: source.endIndex)
133190
else {
134-
throw PreprocessorError.eof(at: cursor, in: source)
191+
throw eofError(at: cursor)
135192
}
136193
guard source[cursor..<endIndex] == expected else {
137-
throw PreprocessorError.expected(expected, at: cursor, in: source)
194+
throw unexpectedCharacterError(expected: expected, character: try peek(), at: cursor)
138195
}
139196
consume(expected.count)
140197
}
141198

142199
func peek() throws -> Character {
143200
guard cursor < source.endIndex else {
144-
throw PreprocessorError.eof(at: cursor, in: source)
201+
throw eofError(at: cursor)
145202
}
146203
return source[cursor]
147204
}
148205

149206
func peek2() throws -> (Character, Character) {
150207
guard cursor < source.endIndex, source.index(after: cursor) < source.endIndex else {
151-
throw PreprocessorError.eof(at: cursor, in: source)
208+
throw eofError(at: cursor)
152209
}
153210
let char1 = source[cursor]
154211
let char2 = source[source.index(after: cursor)]
@@ -205,8 +262,7 @@ private struct Preprocessor {
205262
try expect(" */")
206263
}
207264
guard let token = token else {
208-
throw PreprocessorError(
209-
message: "Unexpected directive", source: source, index: cursor)
265+
throw unexpectedDirectiveError(at: directiveStart)
210266
}
211267
addToken(token, at: directiveStart)
212268
bufferStart = cursor
@@ -221,9 +277,7 @@ private struct Preprocessor {
221277
condition: String, then: [ParseResult], else: [ParseResult], position: String.Index)
222278
}
223279

224-
static func parse(tokens: [TokenInfo], source: String, options: PreprocessOptions) throws
225-
-> [ParseResult]
226-
{
280+
func parse(tokens: [TokenInfo]) throws -> [ParseResult] {
227281
var cursor = tokens.startIndex
228282

229283
func consume() {
@@ -252,14 +306,14 @@ private struct Preprocessor {
252306
}
253307
}
254308
guard case .endif = tokens[cursor].token else {
255-
throw PreprocessorError.unexpected(
256-
token: tokens[cursor].token, at: tokens[cursor].position, in: source)
309+
throw unexpectedTokenError(
310+
expected: .endif, token: tokens[cursor].token, at: tokens[cursor].position)
257311
}
258312
consume()
259313
return .if(condition: condition, then: then, else: `else`, position: ifPosition)
260314
case .else, .endif:
261-
throw PreprocessorError.unexpected(
262-
token: tokens[cursor].token, at: tokens[cursor].position, in: source)
315+
throw unexpectedTokenError(
316+
expected: nil, token: tokens[cursor].token, at: tokens[cursor].position)
263317
}
264318
}
265319
var results: [ParseResult] = []
@@ -269,9 +323,7 @@ private struct Preprocessor {
269323
return results
270324
}
271325

272-
static func preprocess(parsed: [ParseResult], source: String, options: PreprocessOptions) throws
273-
-> String
274-
{
326+
func preprocess(parsed: [ParseResult]) throws -> String {
275327
var result = ""
276328

277329
func appendBlock(content: String) {
@@ -290,8 +342,7 @@ private struct Preprocessor {
290342
appendBlock(content: content)
291343
case .if(let condition, let then, let `else`, let position):
292344
guard let condition = options.variables[condition] else {
293-
throw PreprocessorError.unexpected(
294-
token: .if(condition: condition), at: position, in: source)
345+
throw undefinedVariableError(name: condition, at: position)
295346
}
296347
let blocks = condition ? then : `else`
297348
for block in blocks {

Plugins/PackageToJS/Templates/index.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ export type Options = {
55
* The CLI arguments to pass to the WebAssembly module
66
*/
77
args?: string[]
8+
/* #if USE_SHARED_MEMORY */
9+
/**
10+
* The WebAssembly memory to use (must be 'shared')
11+
*/
12+
memory: WebAssembly.Memory
13+
/* #endif */
814
}
915

1016
/**
@@ -18,7 +24,7 @@ export type Options = {
1824
export declare function init(
1925
moduleSource: WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike<Response>,
2026
imports: Import,
21-
options: Options | undefined
27+
options: Options
2228
): Promise<{
2329
instance: WebAssembly.Instance,
2430
exports: Export

Plugins/PackageToJS/Templates/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export async function init(
1919
new PreopenDirectory("/", new Map()),
2020
], { debug: false })
2121
const { instance, exports, swift } = await instantiate(moduleSource, imports, {
22-
wasi: wasi
22+
wasi,
23+
/* #if USE_SHARED_MEMORY */
24+
memory: options.memory,
25+
/* #endif */
2326
});
2427
swift.main();
2528

Plugins/PackageToJS/Templates/instantiate.d.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ export interface Instantiator {
3131
createExports(instance: WebAssembly.Instance): Export
3232
}
3333

34+
export type InstantiateOptions = {}
35+
3436
/**
3537
* Create an instantiator for the given imports
3638
* @param imports - The imports to add
3739
* @param options - The options
3840
*/
3941
export declare function createInstantiator(
4042
imports: Import,
41-
options: Options | undefined
43+
options: InstantiateOptions | undefined
4244
): Promise<Instantiator>
4345

4446
export interface WASI {
@@ -58,7 +60,10 @@ export declare function instantiate(
5860
moduleSource: WebAssembly.Module | ArrayBufferView | ArrayBuffer | Response | PromiseLike<Response>,
5961
imports: Import,
6062
options: {
61-
wasi: WASI
63+
wasi: WASI,
64+
/* #if USE_SHARED_MEMORY */
65+
memory: WebAssembly.Memory
66+
/* #endif */
6267
}
6368
): Promise<{
6469
instance: WebAssembly.Instance,

Plugins/PackageToJS/Templates/instantiate.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export async function instantiate(
3131
const importObject = {
3232
javascript_kit: swift.wasmImports,
3333
wasi_snapshot_preview1: wasi.wasiImport,
34+
/* #if USE_SHARED_MEMORY */ env: {
35+
memory: options.memory,
36+
},
37+
/* #endif */
3438
};
3539
instantiator.addImports(importObject);
3640

Plugins/PackageToJS/Templates/test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ Please ensure you are using Node.js v18.x or newer.
3939
const dirname = path.dirname(fileURLToPath(import.meta.url))
4040
const { swift } = await instantiate(
4141
await readFile(path.join(dirname, MODULE_PATH)),
42-
{}, { wasi }
42+
{}, {
43+
wasi,
44+
/* #if USE_SHARED_MEMORY */
45+
/* #endif */
46+
}
4347
)
4448
swift.main()
4549
}

0 commit comments

Comments
 (0)