Skip to content

Commit 116c801

Browse files
committed
fix: stub unsafe
Signed-off-by: Christian Stewart <[email protected]>
1 parent b0a74c2 commit 116c801

File tree

7 files changed

+133
-69
lines changed

7 files changed

+133
-69
lines changed

compiler/builtin_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestEmitBuiltinOption(t *testing.T) {
4242
t.Fatalf("Compilation failed: %v", err)
4343
}
4444

45-
// Check that the unsafe package wasn't emitted (we know it's handwritten)
45+
// Check that handwritten packages like unsafe aren't emitted when DisableEmitBuiltin=true
4646
unsafePath := filepath.Join(outputDir, "@goscript/unsafe")
4747
if _, err := os.Stat(unsafePath); !os.IsNotExist(err) {
4848
t.Errorf("unsafe package was emitted when DisableEmitBuiltin=true")

compiler/compiler.go

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -238,50 +238,6 @@ func (c *Compiler) CompilePackages(ctx context.Context, patterns ...string) (*Co
238238
continue
239239
}
240240

241-
// Check if this is the unsafe package, which is not supported in GoScript
242-
if pkg.PkgPath == "unsafe" {
243-
// Find which packages that would actually be compiled depend on unsafe
244-
var dependentPackages []string
245-
for _, otherPkg := range pkgs {
246-
if otherPkg.PkgPath != "unsafe" {
247-
// Check if this package would actually be compiled (same logic as above)
248-
wouldBeCompiled := true
249-
250-
// If the package was not explicitly requested, check if it has a handwritten equivalent
251-
if !slices.Contains(patternPkgPaths, otherPkg.PkgPath) {
252-
gsSourcePath := "gs/" + otherPkg.PkgPath
253-
_, gsErr := gs.GsOverrides.ReadDir(gsSourcePath)
254-
if gsErr == nil {
255-
// Package has handwritten equivalent, so it wouldn't be compiled
256-
wouldBeCompiled = false
257-
}
258-
}
259-
260-
// Skip packages that failed to load
261-
if len(otherPkg.Errors) > 0 {
262-
wouldBeCompiled = false
263-
}
264-
265-
// Only include packages that would actually be compiled and import unsafe
266-
if wouldBeCompiled {
267-
for importPath := range otherPkg.Imports {
268-
if importPath == "unsafe" {
269-
dependentPackages = append(dependentPackages, otherPkg.PkgPath)
270-
break
271-
}
272-
}
273-
}
274-
}
275-
}
276-
277-
dependentList := "unknown package"
278-
if len(dependentPackages) > 0 {
279-
dependentList = strings.Join(dependentPackages, ", ")
280-
}
281-
282-
return nil, fmt.Errorf("cannot compile package 'unsafe': GoScript does not support the unsafe package due to its low-level memory operations that are incompatible with TypeScript/JavaScript. This package is required by: %s. Consider using alternative approaches that don't require unsafe operations", dependentList)
283-
}
284-
285241
pkgCompiler, err := NewPackageCompiler(c.le, &c.config, pkg)
286242
if err != nil {
287243
return nil, fmt.Errorf("failed to create package compiler for %s: %w", pkg.PkgPath, err)

compiler/compiler_test.go

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func getParentGoModulePath() (string, error) {
141141
return strings.TrimSpace(string(output)), nil
142142
}
143143

144-
func TestUnsafePackageErrorMessage(t *testing.T) {
144+
func TestUnsafePackageCompilation(t *testing.T) {
145145
// Create a temporary directory for the test output
146146
tempDir, err := os.MkdirTemp("", "goscript-test-unsafe")
147147
if err != nil {
@@ -154,11 +154,11 @@ func TestUnsafePackageErrorMessage(t *testing.T) {
154154
log.SetLevel(logrus.DebugLevel)
155155
le := logrus.NewEntry(log)
156156

157-
// Test with AllDependencies=true to ensure we get all packages including unsafe
157+
// Test with AllDependencies=true and DisableEmitBuiltin=false to ensure handwritten packages are copied
158158
config := &compiler.Config{
159159
OutputPath: tempDir,
160160
AllDependencies: true,
161-
DisableEmitBuiltin: true, // This ensures handwritten packages are skipped
161+
DisableEmitBuiltin: false, // This ensures handwritten packages are copied to output
162162
}
163163

164164
comp, err := compiler.NewCompiler(config, le, nil)
@@ -168,32 +168,26 @@ func TestUnsafePackageErrorMessage(t *testing.T) {
168168

169169
// Try to compile a package that has dependencies that import unsafe
170170
// We'll use "sync/atomic" which imports unsafe but doesn't have a handwritten equivalent
171-
_, err = comp.CompilePackages(context.Background(), "sync/atomic")
171+
result, err := comp.CompilePackages(context.Background(), "sync/atomic")
172172

173-
// We expect this to fail with an unsafe package error
174-
if err == nil {
175-
t.Fatalf("Expected compilation to fail due to unsafe package, but it succeeded")
173+
// This should now succeed since we have a handwritten unsafe package
174+
if err != nil {
175+
t.Fatalf("Expected compilation to succeed with handwritten unsafe package, but it failed: %v", err)
176176
}
177177

178-
errorMsg := err.Error()
179-
180-
// Verify the error message contains the expected text
181-
if !strings.Contains(errorMsg, "cannot compile package 'unsafe'") {
182-
t.Errorf("Error message should mention unsafe package, got: %s", errorMsg)
178+
// Verify that the unsafe package was copied (not compiled) since it has a handwritten equivalent
179+
if !slices.Contains(result.CopiedPackages, "unsafe") {
180+
t.Errorf("Expected unsafe package to be in CopiedPackages, but it wasn't. CopiedPackages: %v", result.CopiedPackages)
183181
}
184182

185-
// Verify that packages with handwritten equivalents are NOT mentioned in the error
186-
// These packages have handwritten equivalents in gs/ and should not be in the error message
187-
handwrittenPackages := []string{"runtime", "errors", "time", "context", "slices"}
188-
189-
for _, pkg := range handwrittenPackages {
190-
if strings.Contains(errorMsg, pkg) {
191-
t.Errorf("Error message should not mention handwritten package '%s', but it does. Error: %s", pkg, errorMsg)
192-
}
183+
// Verify that sync/atomic was compiled
184+
if !slices.Contains(result.CompiledPackages, "sync/atomic") {
185+
t.Errorf("Expected sync/atomic package to be in CompiledPackages, but it wasn't. CompiledPackages: %v", result.CompiledPackages)
193186
}
194187

195-
// The error message should mention sync/atomic since it would actually be compiled
196-
if !strings.Contains(errorMsg, "sync/atomic") {
197-
t.Errorf("Error message should mention 'sync/atomic' as it would be compiled, got: %s", errorMsg)
188+
// Check that the unsafe package directory exists in the output
189+
unsafePath := filepath.Join(tempDir, "@goscript/unsafe")
190+
if _, err := os.Stat(unsafePath); os.IsNotExist(err) {
191+
t.Errorf("unsafe package directory was not created at %s", unsafePath)
198192
}
199193
}

gs/unsafe/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./unsafe.js"

gs/unsafe/unsafe.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, it, expect } from 'vitest'
2+
import * as unsafe from './unsafe.js'
3+
4+
describe('unsafe package', () => {
5+
it('should throw error for Alignof', () => {
6+
expect(() => unsafe.Alignof({})).toThrow('unsafe.Alignof is not supported in JavaScript/TypeScript')
7+
})
8+
9+
it('should throw error for Offsetof', () => {
10+
expect(() => unsafe.Offsetof({})).toThrow('unsafe.Offsetof is not supported in JavaScript/TypeScript')
11+
})
12+
13+
it('should throw error for Sizeof', () => {
14+
expect(() => unsafe.Sizeof({})).toThrow('unsafe.Sizeof is not supported in JavaScript/TypeScript')
15+
})
16+
17+
it('should throw error for Add', () => {
18+
expect(() => unsafe.Add(null, 1)).toThrow('unsafe.Add is not supported in JavaScript/TypeScript')
19+
})
20+
21+
it('should throw error for Slice', () => {
22+
expect(() => unsafe.Slice(null, 1)).toThrow('unsafe.Slice is not supported in JavaScript/TypeScript')
23+
})
24+
25+
it('should throw error for SliceData', () => {
26+
expect(() => unsafe.SliceData([])).toThrow('unsafe.SliceData is not supported in JavaScript/TypeScript')
27+
})
28+
29+
it('should throw error for String', () => {
30+
expect(() => unsafe.String(null, 1)).toThrow('unsafe.String is not supported in JavaScript/TypeScript')
31+
})
32+
33+
it('should throw error for StringData', () => {
34+
expect(() => unsafe.StringData('test')).toThrow('unsafe.StringData is not supported in JavaScript/TypeScript')
35+
})
36+
37+
it('should export IntegerType as number type', () => {
38+
// IntegerType is just a type alias, so we can't test it directly
39+
// But we can verify it's exported and can be used as a type
40+
const value: unsafe.IntegerType = 42
41+
expect(typeof value).toBe('number')
42+
})
43+
44+
it('should export ArbitraryType and Pointer types', () => {
45+
// These are type aliases, so we can't test them directly
46+
// But we can verify they can be used as types
47+
const arbitrary: unsafe.ArbitraryType = 'anything'
48+
const pointer: unsafe.Pointer = null
49+
expect(arbitrary).toBe('anything')
50+
expect(pointer).toBe(null)
51+
})
52+
})

gs/unsafe/unsafe.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Package unsafe provides facilities for low-level programming including operations
2+
// that violate the type system. Most operations are not supported in JavaScript/TypeScript
3+
// and will throw errors when used.
4+
5+
// ArbitraryType is a shorthand for an arbitrary Go type; it is not a real type
6+
export type ArbitraryType = any;
7+
8+
// Pointer is a pointer type but a Pointer value may not be dereferenced
9+
export type Pointer = any;
10+
11+
// IntegerType is a shorthand for an integer type; it is not a real type
12+
// This is the only type from unsafe that can be meaningfully implemented in JavaScript
13+
export type IntegerType = number;
14+
15+
// Alignof returns the alignment of the (type of the) variable in bytes
16+
// This operation is not meaningful in JavaScript/TypeScript
17+
export function Alignof(variable: ArbitraryType): number {
18+
throw new Error("unsafe.Alignof is not supported in JavaScript/TypeScript: memory alignment is not a meaningful concept in JavaScript");
19+
}
20+
21+
// Offsetof returns the field offset in bytes relative to the struct's address
22+
// This operation is not meaningful in JavaScript/TypeScript
23+
export function Offsetof(selector: ArbitraryType): number {
24+
throw new Error("unsafe.Offsetof is not supported in JavaScript/TypeScript: memory layout and field offsets are not meaningful concepts in JavaScript");
25+
}
26+
27+
// Sizeof returns the size of the (type of the) variable in bytes
28+
// This operation is not meaningful in JavaScript/TypeScript
29+
export function Sizeof(variable: ArbitraryType): number {
30+
throw new Error("unsafe.Sizeof is not supported in JavaScript/TypeScript: memory size is not a meaningful concept in JavaScript");
31+
}
32+
33+
// Add adds len to ptr and returns the updated pointer
34+
// Pointer arithmetic is not supported in JavaScript/TypeScript
35+
export function Add(ptr: Pointer, len: IntegerType): Pointer {
36+
throw new Error("unsafe.Add is not supported in JavaScript/TypeScript: pointer arithmetic is not available in JavaScript");
37+
}
38+
39+
// Slice returns a slice whose underlying array starts at ptr
40+
// This operation is not meaningful in JavaScript/TypeScript
41+
export function Slice(ptr: Pointer, len: IntegerType): any[] {
42+
throw new Error("unsafe.Slice is not supported in JavaScript/TypeScript: direct memory access is not available in JavaScript");
43+
}
44+
45+
// SliceData returns a pointer to the underlying array of the slice
46+
// This operation is not meaningful in JavaScript/TypeScript
47+
export function SliceData(slice: any[]): Pointer {
48+
throw new Error("unsafe.SliceData is not supported in JavaScript/TypeScript: direct memory access is not available in JavaScript");
49+
}
50+
51+
// String returns a string value whose underlying bytes start at ptr
52+
// This operation is not meaningful in JavaScript/TypeScript
53+
export function String(ptr: Pointer, len: IntegerType): string {
54+
throw new Error("unsafe.String is not supported in JavaScript/TypeScript: direct memory access is not available in JavaScript");
55+
}
56+
57+
// StringData returns a pointer to the underlying bytes of the str
58+
// This operation is not meaningful in JavaScript/TypeScript
59+
export function StringData(str: string): Pointer {
60+
throw new Error("unsafe.StringData is not supported in JavaScript/TypeScript: direct memory access is not available in JavaScript");
61+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"test": "npm run test:go && npm run test:js",
5050
"test:go": "go test -v ./...",
5151
"test:js": "npm run typecheck && vitest run",
52-
"typecheck": "tsgo --noEmit",
52+
"typecheck": "tsgo --noEmit -p tsconfig.build.json",
5353
"format": "npm run format:go && npm run format:js && npm run format:config",
5454
"format:config": "prettier --write tsconfig.json package.json",
5555
"format:go": "gofumpt -w .",

0 commit comments

Comments
 (0)