├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── clang-tidy.js ├── examples ├── .gitignore ├── README.md ├── example.js ├── exclaim.js ├── exclaim.test.js ├── jest.config.ts ├── package-lock.json ├── package.json ├── rocJestTransformer.ts └── src │ ├── exclaim.roc │ ├── main.roc │ ├── node-to-roc.c │ ├── platform │ └── main.roc │ └── sayHi.ts ├── package-lock.json ├── package.json ├── src ├── build-roc.ts ├── header.d.ts ├── index.ts ├── node-glue.roc └── node-to-roc.c ├── test.sh ├── tests ├── .gitignore ├── Dockerfile ├── big-str │ ├── main.roc │ ├── platform │ │ └── main.roc │ ├── test.js │ └── test.ts ├── build.js ├── crash │ ├── main.roc │ ├── platform │ │ └── main.roc │ ├── test.js │ └── test.ts ├── json │ ├── main.roc │ ├── platform │ │ └── main.roc │ ├── test.js │ └── test.ts ├── main-in-different-dir │ ├── platform │ │ └── main.roc │ └── test.ts ├── package-lock.json ├── package.json └── small-str │ ├── main.roc │ ├── platform │ └── main.roc │ ├── test.js │ └── test.ts ├── tsconfig.json └── vendor └── glue-platform ├── File.roc ├── InternalTypeId.roc ├── Shape.roc ├── Target.roc ├── TypeId.roc ├── Types.roc └── main.roc /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.node 4 | *.dylib 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vendrinc/roc-esbuild/adc3b55592d962f5e38e9d0b653939c1a8eb2f53/.npmignore -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: "all", 4 | singleQuote: false, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2023 Richard Feldman 2 | 3 | The Universal Permissive License (UPL), Version 1.0 4 | 5 | Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the "Software"), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both 6 | 7 | (a) the Software, and 8 | 9 | (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a “Larger Work” to which the Software is contributed by such licensors), 10 | 11 | without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. 12 | 13 | This license is subject to the following condition: 14 | 15 | The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # roc-esbuild 2 | 3 | This is a work-in-progress [esbuild plugin](https://esbuild.github.io/plugins/) for loading [.roc files](https://roc-lang.org). 4 | -------------------------------------------------------------------------------- /clang-tidy.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const { execSync } = require("child_process") 3 | 4 | const includePath = path.resolve(process.execPath, "..", "..", "include", "node") 5 | 6 | execSync("clang-tidy src/*.c -- -I" + includePath, { stdio: "inherit" }) 7 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.roc.d.ts 3 | *.node 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | To run, `cd` into this directory and then: 4 | 5 | ``` 6 | npm install 7 | npm run start 8 | node dist/bundle.js 9 | npm test 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const esbuild = require("esbuild") 3 | const { roc } = require("roc-esbuild").default 4 | 5 | esbuild 6 | .build({ 7 | entryPoints: [path.join(__dirname, "src", "sayHi.ts")], 8 | bundle: true, 9 | outfile: path.join(__dirname, "dist", "bundle.js"), 10 | sourcemap: "inline", 11 | platform: "node", 12 | minifyWhitespace: true, 13 | treeShaking: true, 14 | plugins: [roc()], 15 | }) 16 | .catch((error) => { 17 | console.error(error) 18 | process.exit(1) 19 | }) 20 | -------------------------------------------------------------------------------- /examples/exclaim.js: -------------------------------------------------------------------------------- 1 | const callRoc = require('./src/exclaim.roc').callRoc; 2 | 3 | module.exports = callRoc; 4 | -------------------------------------------------------------------------------- /examples/exclaim.test.js: -------------------------------------------------------------------------------- 1 | const exclaim = require('./exclaim'); 2 | 3 | test('exclaim("Hi, World") adds a "!" to the end', () => { 4 | expect(exclaim("Hi, World")).toBe("Hi, World!"); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/jest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | import type {Config} from 'jest'; 7 | 8 | const config: Config = { 9 | // All imported modules in your tests should be mocked automatically 10 | // automock: false, 11 | 12 | // Stop running tests after `n` failures 13 | // bail: 0, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/l7/fhxdcfwd0b70w4_bz5kdtmsc0000gn/T/jest_dx", 17 | 18 | // Automatically clear mock calls, instances, contexts and results before every test 19 | // clearMocks: false, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: undefined, 26 | 27 | // The directory where Jest should output its coverage files 28 | // coverageDirectory: undefined, 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // Indicates which provider should be used to instrument code for coverage 36 | coverageProvider: "v8", 37 | 38 | // A list of reporter names that Jest uses when writing coverage reports 39 | // coverageReporters: [ 40 | // "json", 41 | // "text", 42 | // "lcov", 43 | // "clover" 44 | // ], 45 | 46 | // An object that configures minimum threshold enforcement for coverage results 47 | // coverageThreshold: undefined, 48 | 49 | // A path to a custom dependency extractor 50 | // dependencyExtractor: undefined, 51 | 52 | // Make calling deprecated APIs throw helpful error messages 53 | // errorOnDeprecated: false, 54 | 55 | // The default configuration for fake timers 56 | // fakeTimers: { 57 | // "enableGlobally": false 58 | // }, 59 | 60 | // Force coverage collection from ignored files using an array of glob patterns 61 | // forceCoverageMatch: [], 62 | 63 | // A path to a module which exports an async function that is triggered once before all test suites 64 | // globalSetup: undefined, 65 | 66 | // A path to a module which exports an async function that is triggered once after all test suites 67 | // globalTeardown: undefined, 68 | 69 | // A set of global variables that need to be available in all test environments 70 | // globals: {}, 71 | 72 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 73 | // maxWorkers: "50%", 74 | 75 | // An array of directory names to be searched recursively up from the requiring module's location 76 | // moduleDirectories: [ 77 | // "node_modules" 78 | // ], 79 | 80 | // An array of file extensions your modules use 81 | // moduleFileExtensions: [ 82 | // "js", 83 | // "mjs", 84 | // "cjs", 85 | // "jsx", 86 | // "ts", 87 | // "tsx", 88 | // "json", 89 | // "node" 90 | // ], 91 | 92 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 93 | // moduleNameMapper: {}, 94 | 95 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 96 | // modulePathIgnorePatterns: [], 97 | 98 | // Activates notifications for test results 99 | // notify: false, 100 | 101 | // An enum that specifies notification mode. Requires { notify: true } 102 | // notifyMode: "failure-change", 103 | 104 | // A preset that is used as a base for Jest's configuration 105 | // preset: undefined, 106 | 107 | // Run tests from one or more projects 108 | // projects: undefined, 109 | 110 | // Use this configuration option to add custom reporters to Jest 111 | // reporters: undefined, 112 | 113 | // Automatically reset mock state before every test 114 | // resetMocks: false, 115 | 116 | // Reset the module registry before running each individual test 117 | // resetModules: false, 118 | 119 | // A path to a custom resolver 120 | // resolver: undefined, 121 | 122 | // Automatically restore mock state and implementation before every test 123 | // restoreMocks: false, 124 | 125 | // The root directory that Jest should scan for tests and modules within 126 | // rootDir: undefined, 127 | 128 | // A list of paths to directories that Jest should use to search for files in 129 | // roots: [ 130 | // "" 131 | // ], 132 | 133 | // Allows you to use a custom runner instead of Jest's default test runner 134 | // runner: "jest-runner", 135 | 136 | // The paths to modules that run some code to configure or set up the testing environment before each test 137 | // setupFiles: [], 138 | 139 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 140 | // setupFilesAfterEnv: [], 141 | 142 | // The number of seconds after which a test is considered as slow and reported as such in the results. 143 | // slowTestThreshold: 5, 144 | 145 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 146 | // snapshotSerializers: [], 147 | 148 | // The test environment that will be used for testing 149 | // testEnvironment: "jest-environment-node", 150 | 151 | // Options that will be passed to the testEnvironment 152 | // testEnvironmentOptions: {}, 153 | 154 | // Adds a location field to test results 155 | // testLocationInResults: false, 156 | 157 | // The glob patterns Jest uses to detect test files 158 | // testMatch: [ 159 | // "**/__tests__/**/*.[jt]s?(x)", 160 | // "**/?(*.)+(spec|test).[tj]s?(x)" 161 | // ], 162 | 163 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 164 | // testPathIgnorePatterns: [ 165 | // "/node_modules/" 166 | // ], 167 | 168 | // The regexp pattern or array of patterns that Jest uses to detect test files 169 | // testRegex: [], 170 | 171 | // This option allows the use of a custom results processor 172 | // testResultsProcessor: undefined, 173 | 174 | // This option allows use of a custom test runner 175 | // testRunner: "jest-circus/runner", 176 | 177 | // A map from regular expressions to paths to transformers 178 | transform: { 179 | "\\.roc$": "./rocJestTransformer.ts" 180 | } 181 | 182 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 183 | // transformIgnorePatterns: [ 184 | // "/node_modules/", 185 | // "\\.pnp\\.[^\\/]+$" 186 | // ], 187 | 188 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 189 | // unmockedModulePathPatterns: undefined, 190 | 191 | // Indicates whether each individual test should be reported during the run 192 | // verbose: undefined, 193 | 194 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 195 | // watchPathIgnorePatterns: [], 196 | 197 | // Whether to use watchman for file crawling 198 | // watchman: true, 199 | }; 200 | 201 | export default config; 202 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "An example of using the roc-esbuild plgin", 5 | "main": "example.js", 6 | "scripts": { 7 | "start": "node example.js", 8 | "test": "node -r ts-node/register node_modules/.bin/jest --config=jest.config.ts" 9 | }, 10 | "author": "Richard Feldman", 11 | "license": "UPL-1.0", 12 | "devDependencies": { 13 | "esbuild": "^0.14.39", 14 | "jest": "^29.6.1", 15 | "roc-esbuild": "file:..", 16 | "ts-node": "^10.9.1", 17 | "typescript": "^5.2.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/rocJestTransformer.ts: -------------------------------------------------------------------------------- 1 | const buildRocFile = require('../src/build-roc'); 2 | const path = require('path'); 3 | 4 | import { TransformedSource, Transformer } from '@jest/transform'; 5 | 6 | const transformer: Transformer = { 7 | process(src: string, filename: string): TransformedSource { 8 | const addonPath = path.join(path.dirname(filename), "addon.node") 9 | buildRocFile( 10 | filename, 11 | addonPath, 12 | {} // { cc: Array; target: string; optimize: boolean }, 13 | ); 14 | 15 | return { 16 | code: `const addon = require(${JSON.stringify(addonPath)}); module.exports = addon;`, 17 | } 18 | }, 19 | }; 20 | 21 | export default transformer; 22 | -------------------------------------------------------------------------------- /examples/src/exclaim.roc: -------------------------------------------------------------------------------- 1 | app "exclaim" 2 | packages { pf: "platform/main.roc" } 3 | imports [] 4 | provides [main] to pf 5 | 6 | main : Str -> Str 7 | main = \str -> "\(str)!" 8 | -------------------------------------------------------------------------------- /examples/src/main.roc: -------------------------------------------------------------------------------- 1 | app "main" 2 | packages { pf: "platform/main.roc" } 3 | imports [] 4 | provides [main] to pf 5 | 6 | main : Str -> Str 7 | main = \message -> 8 | "TypeScript said to Roc: \(message)! 🎉" 9 | -------------------------------------------------------------------------------- /examples/src/node-to-roc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // If you get an error about node_api.h not being found, run this to find out 13 | // the include path to use: 14 | // 15 | // $ node -e "console.log(path.resolve(process.execPath, '..', '..', 'include', 16 | // 'node'))" 17 | #include 18 | 19 | // This is not volatile because it's only ever set inside a signal handler, 20 | // which according to chatGPT is fine. 21 | // 22 | // (Also, clang gives a warning if it's marked volatile, because 23 | // setjmp and longjmp expect non-volatile arguments, and you can't cast 24 | // the arguments to (jmp_buf) to avoid that warning because jmp_buf is 25 | // an array type, and you can't cast array types.) 26 | jmp_buf jump_on_crash; 27 | 28 | // These are all volatile because they're used in signal handlers but can be set 29 | // outside the signal handler. 30 | volatile int last_signal; 31 | volatile char *last_roc_crash_msg; 32 | 33 | void signal_handler(int sig) { 34 | // Store the signal we encountered, and jump back to the handler 35 | last_signal = sig; 36 | last_roc_crash_msg = NULL; 37 | 38 | longjmp(jump_on_crash, 1); 39 | } 40 | 41 | void *roc_alloc(size_t size, unsigned int u32align) { 42 | size_t align = (size_t)u32align; 43 | 44 | // Note: aligned_alloc only accepts alignments that are 45 | // at least sizeof(void*) and also a power of two, 46 | // so make sure it satisfies both of those. 47 | 48 | // aligned_alloc also requires that the given size is a multiple 49 | // of the alignment, so round to the nearest multiple of align. 50 | size = (size + align - 1) & ~(align - 1); 51 | 52 | return aligned_alloc(align, size); 53 | } 54 | 55 | void *roc_realloc(void *ptr, size_t new_size, size_t old_size, 56 | unsigned int alignment) { 57 | return realloc(ptr, new_size); 58 | } 59 | 60 | void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); } 61 | 62 | void *roc_memcpy(void *dest, const void *src, size_t n) { 63 | return memcpy(dest, src, n); 64 | } 65 | 66 | void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); } 67 | 68 | // Reference counting 69 | 70 | // If the refcount is set to this, that means the allocation is 71 | // stored in readonly memory in the binary, and we must not 72 | // attempt to increment or decrement it; if we do, we'll segfault! 73 | const ssize_t REFCOUNT_READONLY = 0; 74 | const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN; 75 | const size_t MASK = (size_t)PTRDIFF_MIN; 76 | 77 | // Increment reference count, given a pointer to the first element in a 78 | // collection. We don't need to check for overflow because in order to overflow 79 | // a usize worth of refcounts, you'd need to somehow have more pointers in 80 | // memory than the OS's virtual address space can hold. 81 | void incref(uint8_t *bytes, uint32_t alignment) { 82 | ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; 83 | ssize_t refcount = *refcount_ptr; 84 | 85 | if (refcount != REFCOUNT_READONLY) { 86 | *refcount_ptr = refcount + 1; 87 | } 88 | } 89 | 90 | // Decrement reference count, given a pointer to the first byte of a 91 | // collection's elements. Then call roc_dealloc if nothing is referencing this 92 | // collection anymore. 93 | void decref_heap_bytes(uint8_t *bytes, uint32_t alignment) { 94 | size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) 95 | ? sizeof(size_t) 96 | : (size_t)alignment; 97 | ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; 98 | ssize_t refcount = *refcount_ptr; 99 | 100 | if (refcount == REFCOUNT_ONE) { 101 | void *original_allocation = 102 | (void *)(refcount_ptr - (extra_bytes - sizeof(size_t))); 103 | 104 | roc_dealloc(original_allocation, alignment); 105 | } else if (refcount != REFCOUNT_READONLY) { 106 | *refcount_ptr = refcount - 1; 107 | } 108 | } 109 | 110 | // RocBytes (List U8) 111 | 112 | struct RocBytes { 113 | uint8_t *bytes; 114 | size_t len; 115 | size_t capacity; 116 | }; 117 | 118 | struct RocBytes empty_rocbytes() { 119 | struct RocBytes ret = { 120 | .len = 0, 121 | .bytes = NULL, 122 | .capacity = 0, 123 | }; 124 | 125 | return ret; 126 | } 127 | 128 | struct RocBytes init_roc_bytes(uint8_t *bytes, size_t len, size_t capacity) { 129 | if (len == 0) { 130 | return empty_rocbytes(); 131 | } else { 132 | struct RocBytes ret; 133 | size_t refcount_size = sizeof(size_t); 134 | uint8_t *new_refcount = 135 | (uint8_t *)roc_alloc(len + refcount_size, __alignof__(size_t)); 136 | 137 | if (new_refcount == NULL) { 138 | // TODO handle this more gracefully! 139 | fprintf(stderr, "roc_alloc failed during init_roc_bytes in nodeJS; aborting\n"); 140 | abort(); 141 | } 142 | 143 | uint8_t *new_content = new_refcount + refcount_size; 144 | 145 | ((ssize_t *)new_refcount)[0] = REFCOUNT_ONE; 146 | 147 | memcpy(new_content, bytes, len); 148 | 149 | ret.bytes = new_content; 150 | ret.len = len; 151 | ret.capacity = capacity; 152 | 153 | return ret; 154 | } 155 | } 156 | 157 | // RocStr 158 | 159 | struct RocStr { 160 | uint8_t *bytes; 161 | size_t len; 162 | size_t capacity; 163 | }; 164 | 165 | struct RocStr empty_roc_str() { 166 | struct RocStr ret = { 167 | .len = 0, 168 | .bytes = NULL, 169 | .capacity = MASK, 170 | }; 171 | 172 | return ret; 173 | } 174 | 175 | struct RocBytes empty_roc_bytes() { 176 | struct RocBytes ret = { 177 | .len = 0, 178 | .bytes = NULL, 179 | .capacity = MASK, 180 | }; 181 | 182 | return ret; 183 | } 184 | 185 | // Record the small string's length in the last byte of the given stack 186 | // allocation 187 | void write_small_str_len(size_t len, struct RocStr *str) { 188 | ((uint8_t *)str)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000; 189 | } 190 | 191 | struct RocStr roc_str_init_small(uint8_t *bytes, size_t len) { 192 | // Start out with zeroed memory, so that 193 | // if we end up comparing two small RocStr values 194 | // for equality, we won't risk memory garbage resulting 195 | // in two equal strings appearing unequal. 196 | struct RocStr ret = empty_roc_str(); 197 | 198 | // Copy the bytes into the stack allocation 199 | memcpy(&ret, bytes, len); 200 | 201 | write_small_str_len(len, &ret); 202 | 203 | return ret; 204 | } 205 | 206 | struct RocStr roc_str_init_large(uint8_t *bytes, size_t len, size_t capacity) { 207 | // A large RocStr is the same as a List U8 (aka RocBytes) in memory. 208 | struct RocBytes roc_bytes = init_roc_bytes(bytes, len, capacity); 209 | 210 | struct RocStr ret = { 211 | .len = roc_bytes.len, 212 | .bytes = roc_bytes.bytes, 213 | .capacity = roc_bytes.capacity, 214 | }; 215 | 216 | return ret; 217 | } 218 | 219 | bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } 220 | 221 | // Determine the length of the string, taking into 222 | // account the small string optimization 223 | size_t roc_str_len(struct RocStr str) { 224 | uint8_t *bytes = (uint8_t *)&str; 225 | uint8_t last_byte = bytes[sizeof(str) - 1]; 226 | uint8_t last_byte_xored = last_byte ^ 0b10000000; 227 | size_t small_len = (size_t)(last_byte_xored); 228 | size_t big_len = str.len & PTRDIFF_MAX; // Account for seamless slices 229 | 230 | // Avoid branch misprediction costs by always 231 | // determining both small_len and big_len, 232 | // so this compiles to a cmov instruction. 233 | if (is_small_str(str)) { 234 | return small_len; 235 | } else { 236 | return big_len; 237 | } 238 | } 239 | 240 | size_t roc_str_len_small(struct RocStr str) { 241 | uint8_t *bytes = (uint8_t *)&str; 242 | uint8_t last_byte = bytes[sizeof(str) - 1]; 243 | uint8_t last_byte_xored = last_byte ^ 0b10000000; 244 | 245 | return (size_t)(last_byte_xored); 246 | } 247 | 248 | size_t roc_str_len_big(struct RocStr str) { 249 | return str.len & PTRDIFF_MAX; // Account for seamless slices 250 | } 251 | 252 | void decref_large_str(struct RocStr str) { 253 | uint8_t *bytes; 254 | 255 | if ((ssize_t)str.len < 0) { 256 | // This is a seamless slice, so the bytes are located in the capacity slot. 257 | bytes = (uint8_t *)(str.capacity << 1); 258 | } else { 259 | bytes = str.bytes; 260 | } 261 | 262 | decref_heap_bytes(bytes, __alignof__(uint8_t)); 263 | } 264 | 265 | void decref_roc_bytes(struct RocBytes arg) { 266 | uint8_t *bytes; 267 | 268 | if ((ssize_t)arg.len < 0) { 269 | // This is a seamless slice, so the bytes are located in the capacity slot. 270 | bytes = (uint8_t *)(arg.capacity << 1); 271 | } else { 272 | bytes = arg.bytes; 273 | } 274 | 275 | decref_heap_bytes(bytes, __alignof__(uint8_t)); 276 | } 277 | 278 | // Turn the given Node string into a RocStr and write it into the given RocStr 279 | // pointer. 280 | napi_status node_string_into_roc_str(napi_env env, napi_value node_string, 281 | struct RocStr *roc_str) { 282 | size_t len; 283 | napi_status status; 284 | 285 | // Passing NULL for a buffer (and size 0) will make it write the length of the 286 | // string into `len`. 287 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 288 | status = napi_get_value_string_utf8(env, node_string, NULL, 0, &len); 289 | 290 | if (status != napi_ok) { 291 | return status; 292 | } 293 | 294 | // Node's "write a string into this buffer" function always writes a null 295 | // terminator, so capacity will need to be length + 1. 296 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 297 | size_t capacity = len + 1; 298 | 299 | // Create a RocStr and write it into the out param 300 | if (capacity < sizeof(struct RocStr)) { 301 | // If it can fit in a small string, use the string itself as the buffer. 302 | // First, zero out those bytes; small strings need to have zeroes for any 303 | // bytes that are not part of the string, or else comparisons between small 304 | // strings might fail. 305 | *roc_str = empty_roc_str(); 306 | 307 | // This writes the actual number of bytes copied into len. Theoretically 308 | // they should be the same, but it could be different if the buffer was 309 | // somehow smaller. This way we guarantee that the RocStr does not present 310 | // any memory garbage to the user. 311 | status = napi_get_value_string_utf8(env, node_string, (char *)roc_str, 312 | sizeof(struct RocStr), &len); 313 | 314 | if (status != napi_ok) { 315 | return status; 316 | } 317 | 318 | // We have to write the length into the buffer *after* Node copies its bytes 319 | // in, because Node will have written a null terminator, which we may need 320 | // to overwrite. 321 | write_small_str_len(len, roc_str); 322 | } else { 323 | // capacity was too big for a small string, so make a heap allocation and 324 | // write into that. 325 | uint8_t *buf = (uint8_t *)roc_alloc(capacity, __alignof__(char)); 326 | 327 | // This writes the actual number of bytes copied into len. Theoretically 328 | // they should be the same, but it could be different if the buffer was 329 | // somehow smaller. This way we guarantee that the RocStr does not present 330 | // any memory garbage to the user. 331 | status = napi_get_value_string_utf8(env, node_string, (char *)buf, capacity, 332 | &len); 333 | 334 | if (status != napi_ok) { 335 | // Something went wrong, so free the bytes we just allocated before 336 | // returning. 337 | roc_dealloc((void *)buf, __alignof__(char *)); 338 | 339 | return status; 340 | } 341 | 342 | *roc_str = roc_str_init_large(buf, len, capacity); 343 | } 344 | 345 | return status; 346 | } 347 | 348 | // Turn the given Node string into a RocBytes and write it into the given 349 | // RocBytes pointer. 350 | napi_status node_string_into_roc_bytes(napi_env env, napi_value node_string, 351 | struct RocBytes *roc_bytes) { 352 | napi_status status; 353 | size_t len; 354 | 355 | // Passing NULL for a buffer (and size 0) will make it write the length of the 356 | // string into `len`. 357 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 358 | status = napi_get_value_string_utf8(env, node_string, NULL, 0, &len); 359 | 360 | if (status != napi_ok) { 361 | return status; 362 | } 363 | 364 | // Node's "write a string into this buffer" function always writes a null 365 | // terminator, so capacity will need to be length + 1. 366 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 367 | size_t capacity = len + 1; 368 | 369 | // Create a RocBytes and write it into the out param. Make sure we pass 370 | // an align of a pointer, because roc_alloc calls aligned_alloc, which 371 | // will not accept alignment values lower than the align of a pointer! 372 | uint8_t *buf = (uint8_t *)roc_alloc(capacity, __alignof__(uint8_t*)); 373 | 374 | // If allocation failed, bail out. 375 | if (buf == NULL) { 376 | fprintf(stderr, "WARNING: roc_alloc failed during node_string_into_roc_bytes in nodeJS\n"); 377 | return napi_generic_failure; 378 | } 379 | 380 | // This writes the actual number of bytes copied into len. Theoretically 381 | // they should be the same, but it could be different if the buffer was 382 | // somehow smaller. This way we guarantee that the RocBytes does not present 383 | // any memory garbage to the user. 384 | status = 385 | napi_get_value_string_utf8(env, node_string, (char *)buf, capacity, &len); 386 | 387 | if (status != napi_ok) { 388 | // Something went wrong, so free the bytes we just allocated before 389 | // returning. 390 | roc_dealloc((void *)buf, __alignof__(char *)); 391 | 392 | return status; 393 | } 394 | 395 | *roc_bytes = init_roc_bytes(buf, len, capacity); 396 | 397 | return status; 398 | } 399 | 400 | // Consume the given RocStr (decrement its refcount) after creating a Node 401 | // string from it. 402 | napi_value roc_str_into_node_string(napi_env env, struct RocStr roc_str) { 403 | bool is_small = is_small_str(roc_str); 404 | char *roc_str_contents; 405 | 406 | if (is_small) { 407 | // In a small string, the string itself contains its contents. 408 | roc_str_contents = (char *)&roc_str; 409 | } else { 410 | roc_str_contents = (char *)roc_str.bytes; 411 | } 412 | 413 | napi_value answer; 414 | 415 | if (napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_str), 416 | &answer) != napi_ok) { 417 | answer = NULL; 418 | } 419 | 420 | // Decrement the RocStr because we consumed it. 421 | if (!is_small) { 422 | decref_large_str(roc_str); 423 | } 424 | 425 | return answer; 426 | } 427 | 428 | // Consume the given RocBytes (decrement its refcount) after creating a Node 429 | // string from it. (Assume we know these are UTF-8 bytes.) 430 | napi_value roc_bytes_into_node_string(napi_env env, struct RocBytes roc_bytes) { 431 | napi_value answer; 432 | 433 | if (napi_create_string_utf8(env, (char *)roc_bytes.bytes, roc_bytes.len, 434 | &answer) != napi_ok) { 435 | answer = NULL; 436 | } 437 | 438 | // Decrement the RocStr because we consumed it. 439 | decref_roc_bytes(roc_bytes); 440 | 441 | return answer; 442 | } 443 | 444 | // Create a Node string from the given RocStr. 445 | // Don't decrement the RocStr's refcount. (To decrement it, use 446 | // roc_str_into_node_string instead.) 447 | napi_value roc_str_as_node_string(napi_env env, struct RocStr roc_str) { 448 | bool is_small = is_small_str(roc_str); 449 | char *roc_str_contents; 450 | 451 | if (is_small) { 452 | // In a small string, the string itself contains its contents. 453 | roc_str_contents = (char *)&roc_str; 454 | } else { 455 | roc_str_contents = (char *)roc_str.bytes; 456 | } 457 | 458 | napi_status status; 459 | napi_value answer; 460 | 461 | status = napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_str), 462 | &answer); 463 | 464 | if (status != napi_ok) { 465 | return NULL; 466 | } 467 | 468 | // Do not decrement the RocStr's refcount because we did not consume it. 469 | 470 | return answer; 471 | } 472 | 473 | // Create a C string from the given RocStr. Don't reuse memory; do a fresh 474 | // malloc for it. Don't decrement the RocStr's refcount. (To decrement it, use 475 | // roc_str_into_c_string instead.) 476 | char *roc_str_into_c_string(struct RocStr roc_str) { 477 | char *roc_str_contents; 478 | size_t len; 479 | bool is_small = is_small_str(roc_str); 480 | 481 | if (is_small) { 482 | // In a small string, the string itself contains its contents. 483 | roc_str_contents = (char *)&roc_str; 484 | len = roc_str_len_small(roc_str); 485 | } else { 486 | roc_str_contents = (char *)roc_str.bytes; 487 | len = roc_str_len_big(roc_str); 488 | } 489 | 490 | char *buf = (char *)malloc(len + 1); // leave room for the \0 at the end 491 | 492 | // Copy the bytes from the string into the buffer 493 | memcpy(buf, roc_str_contents, len); 494 | 495 | // Write the \0 at the end 496 | buf[len] = '\0'; 497 | 498 | // Decrement the RocStr because we consumed it. 499 | if (!is_small) { 500 | decref_large_str(roc_str); 501 | } 502 | 503 | return buf; 504 | } 505 | 506 | void roc_panic(struct RocStr *roc_str) { 507 | last_signal = 0; 508 | last_roc_crash_msg = roc_str_into_c_string(*roc_str); 509 | 510 | longjmp(jump_on_crash, 1); 511 | } 512 | 513 | extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, 514 | struct RocBytes *arg); 515 | 516 | // Receive a string value from Node and pass it to Roc as a RocStr, then get a 517 | // RocStr back from Roc and convert it into a Node string. 518 | napi_value call_roc(napi_env env, napi_callback_info info) { 519 | // Set the jump point so we can recover from a segfault. 520 | if (setjmp(jump_on_crash) == 0) { 521 | // This is *not* the result of a longjmp 522 | 523 | // Get the argument passed to the Node function 524 | napi_value global, json, stringify, arg_buf[1], node_json_string; 525 | size_t argc = 1; 526 | napi_value argv[1]; 527 | 528 | napi_status status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); 529 | 530 | if (status != napi_ok) { 531 | return NULL; 532 | } 533 | 534 | // Call JSON.stringify(node_arg) 535 | 536 | // Get the global object 537 | if (napi_get_global(env, &global) != napi_ok) { 538 | return NULL; 539 | } 540 | 541 | // global.JSON 542 | if (napi_get_named_property(env, global, "JSON", &json) != napi_ok) { 543 | return NULL; 544 | } 545 | 546 | // global.JSON.stringify 547 | if (napi_get_named_property(env, json, "stringify", &stringify) != 548 | napi_ok) { 549 | return NULL; 550 | } 551 | 552 | // Populate stringify's args 553 | napi_value node_arg = argv[0]; 554 | 555 | arg_buf[0] = node_arg; 556 | 557 | // Call JSON.stringify 558 | if (napi_call_function(env, json, stringify, 1, arg_buf, 559 | &node_json_string) != napi_ok) { 560 | return NULL; 561 | } 562 | 563 | // Translate the JSON string into a Roc List U8 564 | struct RocBytes roc_arg; 565 | 566 | if (node_string_into_roc_bytes(env, node_json_string, &roc_arg) != napi_ok) { 567 | return NULL; 568 | } 569 | 570 | struct RocBytes roc_ret; 571 | 572 | // Call the Roc function to populate `roc_ret`'s bytes. 573 | roc__mainForHost_1_exposed_generic(&roc_ret, &roc_arg); 574 | 575 | // Consume that List U8 to create the Node string. 576 | node_json_string = roc_bytes_into_node_string(env, roc_ret); 577 | 578 | napi_value parse; 579 | 580 | // JSON.parse 581 | if (napi_get_named_property(env, json, "parse", &parse) != napi_ok) { 582 | return NULL; 583 | } 584 | 585 | // Reuse the same arg_buf as last time 586 | arg_buf[0] = node_json_string; 587 | 588 | // Call JSON.parse on what we got back from Roc 589 | napi_value answer; 590 | 591 | if (napi_call_function(env, json, parse, 1, arg_buf, &answer) != napi_ok) { 592 | return NULL; 593 | } 594 | 595 | return answer; 596 | } else { 597 | // This *is* the result of a longjmp 598 | char *msg = last_roc_crash_msg != NULL ? (char *)last_roc_crash_msg 599 | : strsignal(last_signal); 600 | char *suffix = 601 | " while running `main` in a .roc file"; 602 | char *buf = 603 | malloc(strlen(msg) + strlen(suffix) + 1); // +1 for the null terminator 604 | 605 | strcpy(buf, msg); 606 | strcat(buf, suffix); 607 | 608 | napi_throw_error(env, NULL, buf); 609 | 610 | free(buf); 611 | 612 | return NULL; 613 | } 614 | } 615 | 616 | napi_value init(napi_env env, napi_value exports) { 617 | // Before doing anything else, install signal handlers in case subsequent C 618 | // code causes any of these. 619 | struct sigaction action; 620 | memset(&action, 0, sizeof(action)); 621 | action.sa_handler = signal_handler; 622 | 623 | // Handle all the signals that could take out the Node process and translate 624 | // them to exceptions. 625 | sigaction(SIGSEGV, &action, NULL); 626 | sigaction(SIGBUS, &action, NULL); 627 | sigaction(SIGFPE, &action, NULL); 628 | sigaction(SIGILL, &action, NULL); 629 | 630 | // Create our Node functions and expose them from this module. 631 | napi_status status; 632 | napi_value fn; 633 | 634 | status = napi_create_function(env, NULL, 0, call_roc, NULL, &fn); 635 | 636 | if (status != napi_ok) { 637 | return NULL; 638 | } 639 | 640 | status = napi_set_named_property(env, exports, "callRoc", fn); 641 | 642 | if (status != napi_ok) { 643 | return NULL; 644 | } 645 | 646 | return exports; 647 | } 648 | 649 | NAPI_MODULE(NODE_GYP_MODULE_NAME, init) 650 | -------------------------------------------------------------------------------- /examples/src/platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "typescript-interop" 2 | requires {} { main : Str -> Str } 3 | exposes [] 4 | packages {} 5 | imports [TotallyNotJson] 6 | provides [mainForHost] 7 | 8 | mainForHost : Str -> List U8 9 | mainForHost = \arg -> 10 | main arg 11 | |> Encode.toBytes TotallyNotJson.json 12 | -------------------------------------------------------------------------------- /examples/src/sayHi.ts: -------------------------------------------------------------------------------- 1 | import { callRoc } from './main.roc' 2 | 3 | console.log("Roc says the following:", callRoc("Hello from TypeScript")); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roc-esbuild", 3 | "version": "0.0.31", 4 | "description": "Load .roc modules from JS or TS", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": ["dist/*.js", "dist/*.js.map", "dist/*.c", "dist/*.roc", "dist/*.d.ts", "vendor/glue-platform/*.roc"], 8 | "scripts": { 9 | "check": "node clang-tidy.js && clang-format -n src/*.c && tsc --noEmit", 10 | "format": "clang-format -i src/*.c", 11 | "build": "esbuild --bundle src/index.ts --outfile=dist/index.js --platform=node --target=node16.16.0 --sourcemap && tsc --declaration --emitDeclarationOnly --outDir dist && cp src/*.roc dist/ && cp src/*.d.ts dist/ && cp src/*.c dist/", 12 | "dev": "ts-node src/index.ts", 13 | "test": "./test.sh", 14 | "prepublishOnly": "npm run build", 15 | "postinstall": "if [ -d \"src\" ]; then npm run build; else echo 'Build skipped'; fi" 16 | }, 17 | "keywords": [ 18 | "roc", 19 | "esbuild" 20 | ], 21 | "author": "Richard Feldman", 22 | "license": "UPL-1.0", 23 | "dependencies": { 24 | "roc-lang": "0.0.3-2023-12-13-nightly" 25 | }, 26 | "peerDependencies": { 27 | "esbuild": "^0.14.39" 28 | }, 29 | "devDependencies": { 30 | "@ava/typescript": "^4.0.0", 31 | "@types/eslint": "8.4.5", 32 | "@types/node": "^16.11.54", 33 | "@typescript-eslint/eslint-plugin": "5.38.0", 34 | "@typescript-eslint/parser": "5.38.0", 35 | "ava": "^5.3.0", 36 | "eslint": "8.24.0", 37 | "eslint-config-prettier": "8.5.0", 38 | "eslint-import-resolver-typescript": "3.5.1", 39 | "eslint-plugin-import": "2.26.0", 40 | "eslint-plugin-jsdoc": "39.3.6", 41 | "eslint-plugin-markdown": "3.0.0", 42 | "eslint-plugin-mocha": "10.1.0", 43 | "eslint-plugin-node": "11.1.0", 44 | "eslint-plugin-prettier": "4.2.1", 45 | "eslint-plugin-promise": "6.0.1", 46 | "eslint-plugin-tsdoc": "0.2.17", 47 | "ts-node": "^10.9.1", 48 | "typescript": "^5.1.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/build-roc.ts: -------------------------------------------------------------------------------- 1 | // This script does a few things: 2 | // 3 | // 1. Invoke `roc` to build the compiled binary 4 | // 2. Invoke `zig` to convert that binary into a native Node addon (a .node file) 5 | // 3. Copy the binary and its .d.ts type definitions into the appropriate directory 6 | 7 | const fs = require("fs"); 8 | const os = require("os"); 9 | const path = require("path"); 10 | const child_process = require("child_process"); 11 | const util = require("util"); 12 | 13 | const { execSync, spawnSync } = child_process 14 | const execFile = util.promisify(child_process.execFile) 15 | 16 | const ccTargetFromRocTarget = (rocTarget: string) => { 17 | switch (rocTarget) { 18 | case "macos-arm64": 19 | return "aarch64-apple-darwin" 20 | case "macos-x64": 21 | return "x86_64-apple-darwin" 22 | case "linux-arm64": 23 | return "aarch64-linux-gnu" 24 | case "linux-x64": 25 | return "x86_64-linux-gnu" 26 | case "linux-x32": 27 | return "i386-linux-gnu" 28 | case "windows-x64": 29 | return "x86_64-windows-gnu" 30 | case "wasm32": 31 | return "wasm32-unknown-unknown" 32 | case "": 33 | let targetStr = "" 34 | 35 | switch (os.arch()) { 36 | case "arm": 37 | targetStr = "arm" 38 | break 39 | case "arm64": 40 | targetStr = "aarch64" 41 | break 42 | case "x64": 43 | targetStr = "x86_64" 44 | break 45 | case "ia32": 46 | targetStr = "i386" 47 | break 48 | default: 49 | throw new Error(`roc-esbuild does not currently support building for this CPU architecture: ${os.arch()}`) 50 | } 51 | 52 | targetStr += "-" 53 | 54 | switch (os.platform()) { 55 | case "darwin": 56 | targetStr += "macos" 57 | break 58 | case "win32": 59 | targetStr += "win32" 60 | break 61 | case "linux": 62 | targetStr += "linux" 63 | break 64 | default: 65 | throw new Error(`roc-esbuild does not currently support building for this operating system: ${os.platform()}`) 66 | } 67 | 68 | return targetStr 69 | 70 | default: 71 | throw `Unrecognized --target option for roc compiler: ${rocTarget}` 72 | } 73 | } 74 | 75 | const rocNotFoundErr = "roc-esbuild could not find its roc-lang dependency in either its node_modules or any parent node_modules. This means it could not find the `roc` binary it needs to execute!"; 76 | 77 | function runRoc(args: Array) { 78 | let rocLangFile = null; 79 | 80 | try { 81 | // This will return the path of roc-lang's exports: { ".": ... } file 82 | rocLangFile = require.resolve("roc-lang"); 83 | } catch (err) { 84 | throw new Error(rocNotFoundErr); 85 | } 86 | 87 | const rocBinaryPath = rocLangFile ? path.join(path.dirname(rocLangFile), "bin", "roc") : null; 88 | 89 | if (!rocBinaryPath || !fs.existsSync(rocBinaryPath)) { 90 | throw new Error(rocNotFoundErr); 91 | } 92 | 93 | const output = spawnSync(rocBinaryPath, args) 94 | 95 | if (output.status != 0) { 96 | const stdout = output.stdout.toString(); 97 | const stderr = output.stderr.toString(); 98 | const status = output.status === null ? `null, which means the subprocess terminated with a signal (in this case, signal ${output.signal})` : `code ${output.status}` 99 | 100 | throw new Error("`roc " + args.join(" ") + "` exited with status " + status + ". stdout was:\n\n" + stdout + "\n\nstderr was:\n\n" + stderr) 101 | } 102 | } 103 | 104 | const buildRocFile = ( 105 | rocFilePath: string, 106 | addonPath: string, 107 | config: { cc: Array; target: string; optimize: boolean }, 108 | ) => { 109 | // The C compiler to use - e.g. you can specify `["zig" "cc"]` here to use Zig instead of the defualt `cc`. 110 | const cc = config.hasOwnProperty("cc") ? config.cc : ["cc"] 111 | const target = config.hasOwnProperty("target") ? config.target : "" 112 | const optimize = config.hasOwnProperty("optimize") ? config.optimize : "" 113 | 114 | const rocFileName = path.basename(rocFilePath) 115 | const rocFileDir = path.dirname(rocFilePath) 116 | const errors = [] 117 | const buildingForMac = target.startsWith("macos") || (target === "" && os.platform() === "darwin") 118 | const buildingForLinux = target.startsWith("linux") || (target === "" && os.platform() === "linux") 119 | const tmpDir = os.tmpdir() 120 | const rocBuildOutputDir = fs.mkdtempSync(`${tmpDir}${path.sep}`) 121 | const targetSuffix = (target === "" ? "native" : target) 122 | const rocBuildOutputFile = path.join(rocBuildOutputDir, rocFileName.replace(/\.roc$/, `-${targetSuffix}.o`)) 123 | 124 | // Build the initial Roc object binary for the current OS/architecture. 125 | // 126 | // This file may be rebuilt and overridden by a later build step (e.g. when running `yarn package`), but without having 127 | // some object binary here at this step, `node-gyp` (which `npm install`/`yarn install` run automatically, and there's 128 | // no way to disable it) will fail when trying to build the addon, because it will be looking for an object 129 | // binary that isn't there. 130 | runRoc( 131 | [ 132 | "build", 133 | target === "" ? "" : `--target=${target}`, 134 | optimize ? "--optimize" : "", 135 | "--no-link", 136 | "--output", 137 | rocBuildOutputFile, 138 | rocFilePath 139 | ].filter((part) => part !== ""), 140 | ) 141 | 142 | // TODO this is only necessary until `roc glue` can be run on app modules; once that exists, 143 | // we should run glue on the app .roc file and this can go away. 144 | const rocPlatformMain = path.join(rocFileDir, "platform", "main.roc") 145 | 146 | // Generate the C glue 147 | runRoc(["glue", path.join(__dirname, "node-glue.roc"), rocFileDir, rocPlatformMain]) 148 | 149 | // Create the .d.ts file. By design, our glue should output the same .d.ts file regardless of sytem architecture. 150 | const typedefs = 151 | // TODO don't hardcode this, but rather generate it using `roc glue` 152 | `// This file was generated by the esbuild-roc plugin, 153 | // based on the types in the .roc file that has the same 154 | // path as this file but without the .d.ts at the end. 155 | // 156 | // This will be regenerated whenever esbuild runs. 157 | 158 | type JsonValue = boolean | number | string | null | JsonArray | JsonObject 159 | interface JsonArray extends Array {} 160 | interface JsonObject { 161 | [key: string]: JsonValue 162 | } 163 | 164 | // Currently, this function takes whatever you pass it and serializes it to JSON 165 | // for Roc to consume, and then Roc's returned answer also gets serialized to JSON 166 | // before JSON.parse gets called on it to convert it back to a TS value. 167 | // 168 | // This is an "80-20 solution" to get us a lot of functionality without having to 169 | // wait for the nicer version to be implemented. The nicer version would not use 170 | // JSON as an intermediary, and this part would specify the exact TS types needed 171 | // to call the Roc function, based on the Roc function's actual types. 172 | export function callRoc(input: T): U 173 | ` 174 | 175 | fs.writeFileSync(rocFilePath + ".d.ts", typedefs, "utf8") 176 | 177 | // Link the compiled roc binary into a native node addon. This replaces what binding.gyp would do in most 178 | // native node addons, except it can works cross-OS (if { cc: ["zig", "cc"] } is used for the config) 179 | // and runs much faster because it's a single command. 180 | 181 | // NOTE: From here on, everything is built for Node 16.16.0 and may need to be revised to work with future Node versions! 182 | // 183 | // When upgrading node.js versions, here's how to verify what normal node-gyp would do here: 184 | // 185 | // rm -rf build 186 | // npx node-gyp configure 187 | // cd build 188 | // V=1 make 189 | // 190 | // The Makefile that node-gyp generates inside build/ uses a magical V=1 env var to print 191 | // what cc and g++ commands it's doing; this is much nicer than strace output because they 192 | // spawn these processes with mmap and clone3 rather than exceve, so you can't really see 193 | // what the arguments to cc and g++ are from there. 194 | // 195 | // Then basically swap cc for zig cc -target x86_64-linux-gnu, g++ for zig c++ -target x86_64-linux-gnu, 196 | // and adjust the input/output directories as needed. 197 | 198 | // This script is a way to build Native Node Addons without using node-gyp. 199 | // It supports using Zig for cross-platform builds, which enables: 200 | // - Faster builds than node-gyp, which spawns a Python process that generates a Makefile which then runs cc and g++. 201 | // - Building native Node Addons for a given target operating system, without needing to use something like Docker. 202 | // We need this so that we can build Roc addons for Node locally on Apple Silicon developer machines and then ship the 203 | // resulting artifact (inside a zip bundle) directly to an x64 Linux Lambda with no builds taking place on the Labmda. 204 | // We did not want to use Docker for this primarily because of licensing issues, but this also runs faster than building 205 | // on x64 emulation (e.g. using either Docker or podman + qemu). 206 | 207 | // For now, these are hardcoded. In the future we can extract them into a function to call for multiple entrypoints. 208 | const ccTarget = target === "" ? "" : `--target=${ccTargetFromRocTarget(target)}` 209 | const cGluePath = path.join(__dirname, "node-to-roc.c") 210 | const includeRoot = path.resolve(process.execPath, "..", "..") 211 | const includes = [ 212 | "include/node", 213 | // Note: binding-gyp typically includes these other paths as well, and includes them from 214 | // this directory on macOS: ~/Library/Caches/node-gyp/16.20.0 - but note that this 215 | // directory may vary from machine to machine. Finding out where it is probably involves 216 | // spelunking around in the node-gyp source, e.g. https://github.com/nodejs/node-gyp/blob/aaa117c514430aa2c1e568b95df1b6ed1c1fd3b6/lib/configure.js#L268 217 | // seems to be what they use based on the name, but that does not seem likely to be correct. 218 | // 219 | // On the other hand, https://github.com/nodejs/node-gyp/blob/aaa117c514430aa2c1e568b95df1b6ed1c1fd3b6/addon.gypi#L21 220 | // shows a precedent for linking directly to the includes inside the node directory rather than from within 221 | // the binding-gyp directory, but node's directory is missing some of these (e.g. it has no `deps/` directory). 222 | // 223 | // For now, as long as we don't need any of these in our addon, it seems not only fine but actively better to leave 224 | // them out of our build, since it speeds up our build and potentially makes our compiled binary output smaller. 225 | // 226 | // "src", 227 | // "deps/openssl/config", 228 | // "deps/openssl/openssl/include", 229 | // "deps/uv/include", 230 | // "deps/zlib", 231 | // "deps/v8/include" 232 | ] 233 | .map((suffix) => "-I" + path.join(includeRoot, suffix)) 234 | .join(" ") 235 | 236 | const defines = [ 237 | // TODO this should be dynamic, not hardcoded to "addon" - see: 238 | // https://nodejs.org/api/n-api.html#module-registration 239 | "NODE_GYP_MODULE_NAME=addon", 240 | "USING_UV_SHARED=1", 241 | "USING_V8_SHARED=1", 242 | "V8_DEPRECATION_WARNINGS=1", 243 | "V8_DEPRECATION_WARNINGS", 244 | "V8_IMMINENT_DEPRECATION_WARNINGS", 245 | "_GLIBCXX_USE_CXX11_ABI=1", 246 | "_DARWIN_USE_64_BIT_INODE=1", 247 | "_LARGEFILE_SOURCE", 248 | "_FILE_OFFSET_BITS=64", 249 | "__STDC_FORMAT_MACROS", 250 | "OPENSSL_NO_PINSHARED", 251 | "OPENSSL_THREADS", 252 | "BUILDING_NODE_EXTENSION", 253 | ] 254 | .map((flag) => "-D'" + flag + "'") 255 | .join(" ") 256 | 257 | const libraries = ["c", "m", "pthread", "dl", "util"].map((library) => "-l" + library) 258 | 259 | if (buildingForLinux) { 260 | // Linux requires -lrt 261 | libraries.push("-lrt") 262 | } 263 | 264 | const cmd = cc 265 | .concat([ 266 | ccTarget === "" ? "" : ccTarget, 267 | "-o", 268 | addonPath, 269 | rocBuildOutputFile, 270 | cGluePath, 271 | defines, 272 | includes, 273 | "-fPIC", 274 | "-pthread", 275 | optimize ? "-O3" : "", 276 | // This was in the original node-gyp build, but it generates a separate directory. 277 | // (Maybe it also adds the symbols to the binary? Further investigation needed.) 278 | // buildingForMac ? "-gdwarf-2" : "", 279 | 280 | // Many roc hosts need aligned_alloc, which was added in macOS 10.15. 281 | buildingForMac ? "-mmacosx-version-min=10.15" : "", 282 | "-Wall", 283 | "-Wextra", 284 | "-Wendif-labels", 285 | "-W", 286 | "-Wno-unused-parameter", 287 | buildingForMac ? "-fno-strict-aliasing" : "-fno-omit-frame-pointer", 288 | buildingForMac ? "-Wl,-undefined,dynamic_lookup" : "", 289 | libraries.join(" "), 290 | buildingForLinux ? "-shared" : "", 291 | ]) 292 | .flat() 293 | .filter((part) => part !== "") 294 | .join(" ") 295 | 296 | // Compile the node <-> roc C bridge and statically link in the .o binary (produced by `roc`) 297 | // into the .node addon binary 298 | execSync(cmd, { stdio: "inherit" }) 299 | 300 | return { errors: [] } 301 | } 302 | 303 | module.exports = buildRocFile 304 | -------------------------------------------------------------------------------- /src/header.d.ts: -------------------------------------------------------------------------------- 1 | // ⚠️ This file was generated by `roc glue`, 2 | // based on the types in the .roc file that has the same 3 | // path as this file but without the .d.ts at the end. 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // This plugin does a few things: 2 | // 3 | // 1. Invoke `roc` to build the compiled binary 4 | // 2. Invoke `cc` to convert that binary into a native Node addon (a .node file) 5 | // 3. Copy the binary and its .d.ts type definitions into the appropriate directory 6 | 7 | import type { PluginBuild, Plugin } from "esbuild"; 8 | import path from "path" 9 | 10 | const buildRocFile = require("./build-roc") 11 | const rocNodeFileNamespace = "roc-node-file" 12 | 13 | function roc(opts?: { cc?: Array; target?: string, optimize?: boolean }) : Plugin { 14 | const config = opts !== undefined ? opts : {} 15 | 16 | return { 17 | name: "roc", 18 | setup(build: PluginBuild) { 19 | // Resolve ".roc" files to a ".node" path with a namespace 20 | build.onResolve({ filter: /\.roc$/, namespace: "file" }, (args) => { 21 | return { 22 | path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, path.dirname(args.path), path.basename(args.path).replace(/\.roc$/, ".node")), 23 | namespace: rocNodeFileNamespace, 24 | } 25 | }) 26 | 27 | // Files in the "node-file" virtual namespace call "require()" on the 28 | // path from esbuild of the ".node" file in the output directory. 29 | // Strategy adapted from https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487 30 | build.onLoad({ filter: /.*/, namespace: rocNodeFileNamespace }, (args) => { 31 | // Load ".roc" files, generate .d.ts files for them, compile and link them into native Node addons, 32 | // and tell esbuild how to bundle those addons. 33 | const rocFilePath = args.path.replace(/\.node$/, ".roc") 34 | const { errors } = buildRocFile(rocFilePath, args.path, config) // TODO get `target` arg from esbuild config 35 | 36 | return { 37 | contents: ` 38 | import path from ${JSON.stringify(args.path)} 39 | module.exports = require(path) 40 | `, 41 | } 42 | }) 43 | 44 | // If a ".node" file is imported within a module in the "roc-node-file" namespace, put 45 | // it in the "file" namespace where esbuild's default loading behavior will handle 46 | // it. It is already an absolute path since we resolved it to one earlier. 47 | build.onResolve({ filter: /\.node$/, namespace: rocNodeFileNamespace }, (args) => ({ 48 | path: args.path, 49 | namespace: "file", 50 | })) 51 | 52 | // Use the `file` loader for .node files by default. 53 | let opts = build.initialOptions 54 | 55 | opts.loader = opts.loader || {} 56 | 57 | opts.loader[".node"] = "file" 58 | }, 59 | } 60 | } 61 | 62 | export default { roc, buildRocFile } 63 | -------------------------------------------------------------------------------- /src/node-glue.roc: -------------------------------------------------------------------------------- 1 | app "node-glue" 2 | packages { pf: "../vendor/glue-platform/main.roc" } 3 | imports [ 4 | pf.Types.{ Types }, 5 | pf.Shape.{ Shape }, # , RocStructFields }, 6 | pf.TypeId.{ TypeId }, 7 | pf.File.{ File }, 8 | "header.d.ts" as dtsHeader : Str, 9 | ] 10 | provides [makeGlue] to pf 11 | 12 | makeGlue : List Types -> Result (List File) Str 13 | makeGlue = \typesByArch -> 14 | dtsFileContent = 15 | List.walk typesByArch dtsHeader \buf, types -> 16 | arch = (Types.target types).architecture 17 | 18 | # Always use aarch64 for now, and ignore the others. 19 | # This is because we only want to generate one .d.ts 20 | # file (not a different one per architecture), and 21 | # we want it to use 64-bit assumptions (specifically, `Nat` 22 | # becomes `bignum` instead of `number`) becuase those 23 | # assumptions will match what happens when `Nat` becomes `U64`. 24 | if arch == Aarch64 then 25 | buf 26 | |> addEntryPoints types 27 | else 28 | buf 29 | 30 | Ok [ 31 | { 32 | # TODO get the input filename and make the output .d.ts file be based on that 33 | name: "main.roc.d.ts", 34 | content: dtsFileContent, 35 | }, 36 | ] 37 | 38 | addEntryPoints : Str, Types -> Str 39 | addEntryPoints = \buf, types -> 40 | List.walk (Types.entryPoints types) buf \state, T name id -> 41 | addEntryPoint state types name id 42 | 43 | addEntryPoint : Str, Types, Str, TypeId -> Str 44 | addEntryPoint = \buf, types, name, id -> 45 | signature = 46 | when Types.shape types id is 47 | Function rocFn -> 48 | arguments = 49 | toArgStr rocFn.args types \argId, _shape, index -> 50 | type = typeName types argId 51 | indexStr = Num.toStr index 52 | 53 | "arg\(indexStr): \(type)" 54 | 55 | ret = typeName types rocFn.ret 56 | 57 | "(\(arguments)): \(ret)" 58 | 59 | _ -> 60 | ret = typeName types id 61 | "(): \(ret)" 62 | 63 | "\(buf)\nexport function \(name)\(signature);\n" 64 | 65 | # addShape : Types -> (Str, Shape, TypeId -> Str) 66 | # addShape = \types -> \buf, shape, typeId -> 67 | # when shape is 68 | # Num U8 | Num I8 | Num U16 | Num I16 | Num U32 | Num I32 | Num F32 | Num F64 -> "number", 69 | # Bool -> "boolean", 70 | # RocStr -> "string", 71 | # # Arguably Unit should be `void` in some contexts (e.g. Promises and return types), 72 | # # but then again, why would you ever have a Roc function that returns {}? Perhaps more 73 | # # relevantly, a Roc function that accepts {} as its argument should accept 0 arguments in TS. 74 | # # For now, this works because if you pass nothing, it will be like passing undefined. 75 | # Unit -> "undefined" 76 | # Struct { name, fields } -> 77 | # generateRecord buf types typeId name fields Public 78 | 79 | # Function rocFn -> 80 | # if rocFn.isToplevel then 81 | # buf # We already generated a type for this, so don't do it again! 82 | # else 83 | # generateFunction buf types rocFn 84 | 85 | # _ -> # TODO finish these 86 | # crash "`roc glue` encountered a type that it doesn't yet know how to translate to a TypeScript type!" 87 | 88 | # generateRecord : Str, Types, TypeId, Str, RocStructFields, [Public, Private] -> Str 89 | # generateRecord = \buf, types, id, name, fields, privacy -> 90 | # "" 91 | 92 | typeName : Types, TypeId -> Str 93 | typeName = \types, id -> 94 | when Types.shape types id is 95 | Bool -> "boolean" 96 | RocStr -> "string" 97 | Num U8 | Num I8 | Num U16 | Num I16 | Num U32 | Num I32 | Num F32 | Num F64 -> "number" 98 | Num U64 | Num I64 | Num U128 | Num I128 -> "BigInt" 99 | Num Dec -> crash "TODO convert from Roc Dec to some JavaScript type (possibly a C wrapper around RocDec?)" 100 | # Arguably Unit should be `void` in some contexts (e.g. Promises and return types), 101 | # but then again, why would you ever have a Roc function that returns {}? Perhaps more 102 | # relevantly, a Roc function that accepts {} as its argument should accept 0 arguments in TS. 103 | # For now, this works because if you pass nothing, it will be like passing undefined. 104 | Unit -> "undefined" 105 | Unsized -> "Uint8Array" # opaque list of bytes (List in Roc) 106 | EmptyTagUnion -> "never" 107 | RocList elemTypeId -> 108 | when Types.shape types elemTypeId is 109 | Num U8 -> "Uint8Array" 110 | Num I8 -> "Int8Array" 111 | Num U16 -> "Uint16Array" 112 | Num I16 -> "Int16Array" 113 | Num U32 -> "Uint32Array" 114 | Num I32 -> "Int32Array" 115 | Num U64 -> "BigUint64Array" 116 | Num I64 -> "BigInt64Array" 117 | Num F32 -> "Float32Array" 118 | Num F64 -> "Float64Array" 119 | _ -> "Array<\(typeName types elemTypeId)>" 120 | 121 | RocDict key value -> "Map<\(typeName types key), \(typeName types value)>" 122 | RocSet elem -> "Set<\(typeName types elem)>" 123 | RocBox _elem -> crash "TODO generate types for RocBox" 124 | RocResult _ok _err -> crash "TODO generate types for RocResult" 125 | RecursivePointer content -> typeName types content 126 | Struct { fields } -> 127 | when fields is 128 | HasNoClosure list -> recordTypeName types list 129 | HasClosure list -> recordTypeName types list 130 | 131 | TagUnionPayload { name } -> escapeKW name 132 | TagUnion (NonRecursive { name }) -> escapeKW name 133 | TagUnion (Recursive { name }) -> escapeKW name 134 | TagUnion (Enumeration { name }) -> escapeKW name 135 | TagUnion (NullableWrapped { name }) -> escapeKW name 136 | TagUnion (NullableUnwrapped { name }) -> escapeKW name 137 | TagUnion (NonNullableUnwrapped { name }) -> escapeKW name 138 | TagUnion (SingleTagStruct { name }) -> escapeKW name 139 | Function { functionName } -> escapeKW functionName 140 | 141 | escapeKW : Str -> Str 142 | escapeKW = \input -> 143 | if Set.contains reservedKeywords input then 144 | "roc_\(input)" 145 | else 146 | input 147 | 148 | reservedKeywords : Set Str 149 | reservedKeywords = Set.fromList [ 150 | "break", 151 | "case", 152 | "catch", 153 | "class", 154 | "const", 155 | "continue", 156 | "debugger", 157 | "default", 158 | "delete", 159 | "do", 160 | "else", 161 | "export", 162 | "extends", 163 | "finally", 164 | "for", 165 | "function", 166 | "if", 167 | "import", 168 | "in", 169 | "instanceof", 170 | "let", 171 | "new", 172 | "return", 173 | "super", 174 | "switch", 175 | "this", 176 | "throw", 177 | "try", 178 | "typeof", 179 | "var", 180 | "void", 181 | "while", 182 | "with", 183 | "yield", 184 | "implements", 185 | "interface", 186 | "package", 187 | "private", 188 | "protected", 189 | "public", 190 | "static", 191 | "async", 192 | "await", 193 | "get", 194 | "set", 195 | "of", 196 | "enum", 197 | "as", 198 | "from", 199 | "null", 200 | "true", 201 | "false", 202 | ] 203 | 204 | toArgStr : List TypeId, Types, (TypeId, Shape, Nat -> Str) -> Str 205 | toArgStr = \args, types, fmt -> 206 | List.walkWithIndex args "" \state, argId, index -> 207 | shape = Types.shape types argId 208 | 209 | # Drop `{}` args, as JavaScript has no equivalent of passing {}; instead, they would just not accept anything. 210 | if isUnit shape then 211 | state 212 | else 213 | argStr = fmt argId shape index 214 | 215 | if Str.isEmpty state then 216 | argStr # Don't prepend a comma if this is the first one 217 | else 218 | state 219 | |> Str.concat ", " 220 | |> Str.concat argStr 221 | 222 | recordTypeName : Types, List { name : Str, id : TypeId }* -> Str 223 | recordTypeName = \types, fields -> 224 | fieldTypes = 225 | fields 226 | |> List.map \{name, id} -> "\(name): \(typeName types id)" 227 | |> Str.joinWith ", " 228 | 229 | "{ \(fieldTypes) }" 230 | 231 | isUnit : Shape -> Bool 232 | isUnit = \shape -> 233 | when shape is 234 | Unit -> Bool.true 235 | _ -> Bool.false 236 | -------------------------------------------------------------------------------- /src/node-to-roc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // If you get an error about node_api.h not being found, run this to find out 13 | // the include path to use: 14 | // 15 | // $ node -e "console.log(path.resolve(process.execPath, '..', '..', 'include', 16 | // 'node'))" 17 | #include 18 | 19 | // This is not volatile because it's only ever set inside a signal handler, 20 | // which according to chatGPT is fine. 21 | // 22 | // (Also, clang gives a warning if it's marked volatile, because 23 | // setjmp and longjmp expect non-volatile arguments, and you can't cast 24 | // the arguments to (jmp_buf) to avoid that warning because jmp_buf is 25 | // an array type, and you can't cast array types.) 26 | jmp_buf jump_on_crash; 27 | 28 | // These are all volatile because they're used in signal handlers but can be set 29 | // outside the signal handler. 30 | volatile int last_signal; 31 | volatile char *last_roc_crash_msg; 32 | 33 | void signal_handler(int sig) { 34 | // Store the signal we encountered, and jump back to the handler 35 | last_signal = sig; 36 | last_roc_crash_msg = NULL; 37 | 38 | longjmp(jump_on_crash, 1); 39 | } 40 | 41 | void *roc_alloc(size_t size, unsigned int u32align) { 42 | size_t align = (size_t)u32align; 43 | 44 | // Note: aligned_alloc only accepts alignments that are 45 | // at least sizeof(void*) and also a power of two, 46 | // so make sure it satisfies both of those. 47 | 48 | // aligned_alloc also requires that the given size is a multiple 49 | // of the alignment, so round to the nearest multiple of align. 50 | size = (size + align - 1) & ~(align - 1); 51 | 52 | return aligned_alloc(align, size); 53 | } 54 | 55 | void *roc_realloc(void *ptr, size_t new_size, size_t old_size, 56 | unsigned int alignment) { 57 | return realloc(ptr, new_size); 58 | } 59 | 60 | void roc_dealloc(void *ptr, unsigned int alignment) { free(ptr); } 61 | 62 | void *roc_memcpy(void *dest, const void *src, size_t n) { 63 | return memcpy(dest, src, n); 64 | } 65 | 66 | void *roc_memset(void *str, int c, size_t n) { return memset(str, c, n); } 67 | 68 | // Reference counting 69 | 70 | // If the refcount is set to this, that means the allocation is 71 | // stored in readonly memory in the binary, and we must not 72 | // attempt to increment or decrement it; if we do, we'll segfault! 73 | const ssize_t REFCOUNT_READONLY = 0; 74 | const ssize_t REFCOUNT_ONE = (ssize_t)PTRDIFF_MIN; 75 | const size_t MASK = (size_t)PTRDIFF_MIN; 76 | 77 | // Increment reference count, given a pointer to the first element in a 78 | // collection. We don't need to check for overflow because in order to overflow 79 | // a usize worth of refcounts, you'd need to somehow have more pointers in 80 | // memory than the OS's virtual address space can hold. 81 | void incref(uint8_t *bytes, uint32_t alignment) { 82 | ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; 83 | ssize_t refcount = *refcount_ptr; 84 | 85 | if (refcount != REFCOUNT_READONLY) { 86 | *refcount_ptr = refcount + 1; 87 | } 88 | } 89 | 90 | // Decrement reference count, given a pointer to the first byte of a 91 | // collection's elements. Then call roc_dealloc if nothing is referencing this 92 | // collection anymore. 93 | void decref_heap_bytes(uint8_t *bytes, uint32_t alignment) { 94 | size_t extra_bytes = (sizeof(size_t) >= (size_t)alignment) 95 | ? sizeof(size_t) 96 | : (size_t)alignment; 97 | ssize_t *refcount_ptr = ((ssize_t *)bytes) - 1; 98 | ssize_t refcount = *refcount_ptr; 99 | 100 | if (refcount == REFCOUNT_ONE) { 101 | void *original_allocation = 102 | (void *)(refcount_ptr - (extra_bytes - sizeof(size_t))); 103 | 104 | roc_dealloc(original_allocation, alignment); 105 | } else if (refcount != REFCOUNT_READONLY) { 106 | *refcount_ptr = refcount - 1; 107 | } 108 | } 109 | 110 | // RocBytes (List U8) 111 | 112 | struct RocBytes { 113 | uint8_t *bytes; 114 | size_t len; 115 | size_t capacity; 116 | }; 117 | 118 | struct RocBytes empty_rocbytes() { 119 | struct RocBytes ret = { 120 | .len = 0, 121 | .bytes = NULL, 122 | .capacity = 0, 123 | }; 124 | 125 | return ret; 126 | } 127 | 128 | struct RocBytes init_roc_bytes(uint8_t *bytes, size_t len, size_t capacity) { 129 | if (len == 0) { 130 | return empty_rocbytes(); 131 | } else { 132 | struct RocBytes ret; 133 | size_t refcount_size = sizeof(size_t); 134 | uint8_t *new_refcount = 135 | (uint8_t *)roc_alloc(len + refcount_size, __alignof__(size_t)); 136 | 137 | if (new_refcount == NULL) { 138 | // TODO handle this more gracefully! 139 | fprintf(stderr, "roc_alloc failed during init_roc_bytes in nodeJS; aborting\n"); 140 | abort(); 141 | } 142 | 143 | uint8_t *new_content = new_refcount + refcount_size; 144 | 145 | ((ssize_t *)new_refcount)[0] = REFCOUNT_ONE; 146 | 147 | memcpy(new_content, bytes, len); 148 | 149 | ret.bytes = new_content; 150 | ret.len = len; 151 | ret.capacity = capacity; 152 | 153 | return ret; 154 | } 155 | } 156 | 157 | // RocStr 158 | 159 | struct RocStr { 160 | uint8_t *bytes; 161 | size_t len; 162 | size_t capacity; 163 | }; 164 | 165 | struct RocStr empty_roc_str() { 166 | struct RocStr ret = { 167 | .len = 0, 168 | .bytes = NULL, 169 | .capacity = MASK, 170 | }; 171 | 172 | return ret; 173 | } 174 | 175 | struct RocBytes empty_roc_bytes() { 176 | struct RocBytes ret = { 177 | .len = 0, 178 | .bytes = NULL, 179 | .capacity = MASK, 180 | }; 181 | 182 | return ret; 183 | } 184 | 185 | // Record the small string's length in the last byte of the given stack 186 | // allocation 187 | void write_small_str_len(size_t len, struct RocStr *str) { 188 | ((uint8_t *)str)[sizeof(struct RocStr) - 1] = (uint8_t)len | 0b10000000; 189 | } 190 | 191 | struct RocStr roc_str_init_small(uint8_t *bytes, size_t len) { 192 | // Start out with zeroed memory, so that 193 | // if we end up comparing two small RocStr values 194 | // for equality, we won't risk memory garbage resulting 195 | // in two equal strings appearing unequal. 196 | struct RocStr ret = empty_roc_str(); 197 | 198 | // Copy the bytes into the stack allocation 199 | memcpy(&ret, bytes, len); 200 | 201 | write_small_str_len(len, &ret); 202 | 203 | return ret; 204 | } 205 | 206 | struct RocStr roc_str_init_large(uint8_t *bytes, size_t len, size_t capacity) { 207 | // A large RocStr is the same as a List U8 (aka RocBytes) in memory. 208 | struct RocBytes roc_bytes = init_roc_bytes(bytes, len, capacity); 209 | 210 | struct RocStr ret = { 211 | .len = roc_bytes.len, 212 | .bytes = roc_bytes.bytes, 213 | .capacity = roc_bytes.capacity, 214 | }; 215 | 216 | return ret; 217 | } 218 | 219 | bool is_small_str(struct RocStr str) { return ((ssize_t)str.capacity) < 0; } 220 | 221 | // Determine the length of the string, taking into 222 | // account the small string optimization 223 | size_t roc_str_len(struct RocStr str) { 224 | uint8_t *bytes = (uint8_t *)&str; 225 | uint8_t last_byte = bytes[sizeof(str) - 1]; 226 | uint8_t last_byte_xored = last_byte ^ 0b10000000; 227 | size_t small_len = (size_t)(last_byte_xored); 228 | size_t big_len = str.len & PTRDIFF_MAX; // Account for seamless slices 229 | 230 | // Avoid branch misprediction costs by always 231 | // determining both small_len and big_len, 232 | // so this compiles to a cmov instruction. 233 | if (is_small_str(str)) { 234 | return small_len; 235 | } else { 236 | return big_len; 237 | } 238 | } 239 | 240 | size_t roc_str_len_small(struct RocStr str) { 241 | uint8_t *bytes = (uint8_t *)&str; 242 | uint8_t last_byte = bytes[sizeof(str) - 1]; 243 | uint8_t last_byte_xored = last_byte ^ 0b10000000; 244 | 245 | return (size_t)(last_byte_xored); 246 | } 247 | 248 | size_t roc_str_len_big(struct RocStr str) { 249 | return str.len & PTRDIFF_MAX; // Account for seamless slices 250 | } 251 | 252 | void decref_large_str(struct RocStr str) { 253 | uint8_t *bytes; 254 | 255 | if ((ssize_t)str.len < 0) { 256 | // This is a seamless slice, so the bytes are located in the capacity slot. 257 | bytes = (uint8_t *)(str.capacity << 1); 258 | } else { 259 | bytes = str.bytes; 260 | } 261 | 262 | decref_heap_bytes(bytes, __alignof__(uint8_t)); 263 | } 264 | 265 | void decref_roc_bytes(struct RocBytes arg) { 266 | uint8_t *bytes; 267 | 268 | if ((ssize_t)arg.len < 0) { 269 | // This is a seamless slice, so the bytes are located in the capacity slot. 270 | bytes = (uint8_t *)(arg.capacity << 1); 271 | } else { 272 | bytes = arg.bytes; 273 | } 274 | 275 | decref_heap_bytes(bytes, __alignof__(uint8_t)); 276 | } 277 | 278 | // Turn the given Node string into a RocStr and write it into the given RocStr 279 | // pointer. 280 | napi_status node_string_into_roc_str(napi_env env, napi_value node_string, 281 | struct RocStr *roc_str) { 282 | size_t len; 283 | napi_status status; 284 | 285 | // Passing NULL for a buffer (and size 0) will make it write the length of the 286 | // string into `len`. 287 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 288 | status = napi_get_value_string_utf8(env, node_string, NULL, 0, &len); 289 | 290 | if (status != napi_ok) { 291 | return status; 292 | } 293 | 294 | // Node's "write a string into this buffer" function always writes a null 295 | // terminator, so capacity will need to be length + 1. 296 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 297 | size_t capacity = len + 1; 298 | 299 | // Create a RocStr and write it into the out param 300 | if (capacity < sizeof(struct RocStr)) { 301 | // If it can fit in a small string, use the string itself as the buffer. 302 | // First, zero out those bytes; small strings need to have zeroes for any 303 | // bytes that are not part of the string, or else comparisons between small 304 | // strings might fail. 305 | *roc_str = empty_roc_str(); 306 | 307 | // This writes the actual number of bytes copied into len. Theoretically 308 | // they should be the same, but it could be different if the buffer was 309 | // somehow smaller. This way we guarantee that the RocStr does not present 310 | // any memory garbage to the user. 311 | status = napi_get_value_string_utf8(env, node_string, (char *)roc_str, 312 | sizeof(struct RocStr), &len); 313 | 314 | if (status != napi_ok) { 315 | return status; 316 | } 317 | 318 | // We have to write the length into the buffer *after* Node copies its bytes 319 | // in, because Node will have written a null terminator, which we may need 320 | // to overwrite. 321 | write_small_str_len(len, roc_str); 322 | } else { 323 | // capacity was too big for a small string, so make a heap allocation and 324 | // write into that. 325 | uint8_t *buf = (uint8_t *)roc_alloc(capacity, __alignof__(char)); 326 | 327 | // This writes the actual number of bytes copied into len. Theoretically 328 | // they should be the same, but it could be different if the buffer was 329 | // somehow smaller. This way we guarantee that the RocStr does not present 330 | // any memory garbage to the user. 331 | status = napi_get_value_string_utf8(env, node_string, (char *)buf, capacity, 332 | &len); 333 | 334 | if (status != napi_ok) { 335 | // Something went wrong, so free the bytes we just allocated before 336 | // returning. 337 | roc_dealloc((void *)buf, __alignof__(char *)); 338 | 339 | return status; 340 | } 341 | 342 | *roc_str = roc_str_init_large(buf, len, capacity); 343 | } 344 | 345 | return status; 346 | } 347 | 348 | // Turn the given Node string into a RocBytes and write it into the given 349 | // RocBytes pointer. 350 | napi_status node_string_into_roc_bytes(napi_env env, napi_value node_string, 351 | struct RocBytes *roc_bytes) { 352 | napi_status status; 353 | size_t len; 354 | 355 | // Passing NULL for a buffer (and size 0) will make it write the length of the 356 | // string into `len`. 357 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 358 | status = napi_get_value_string_utf8(env, node_string, NULL, 0, &len); 359 | 360 | if (status != napi_ok) { 361 | return status; 362 | } 363 | 364 | // Node's "write a string into this buffer" function always writes a null 365 | // terminator, so capacity will need to be length + 1. 366 | // https://nodejs.org/api/n-api.html#napi_get_value_string_utf8 367 | size_t capacity = len + 1; 368 | 369 | // Create a RocBytes and write it into the out param. Make sure we pass 370 | // an align of a pointer, because roc_alloc calls aligned_alloc, which 371 | // will not accept alignment values lower than the align of a pointer! 372 | uint8_t *buf = (uint8_t *)roc_alloc(capacity, __alignof__(uint8_t*)); 373 | 374 | // If allocation failed, bail out. 375 | if (buf == NULL) { 376 | fprintf(stderr, "WARNING: roc_alloc failed during node_string_into_roc_bytes in nodeJS\n"); 377 | return napi_generic_failure; 378 | } 379 | 380 | // This writes the actual number of bytes copied into len. Theoretically 381 | // they should be the same, but it could be different if the buffer was 382 | // somehow smaller. This way we guarantee that the RocBytes does not present 383 | // any memory garbage to the user. 384 | status = 385 | napi_get_value_string_utf8(env, node_string, (char *)buf, capacity, &len); 386 | 387 | if (status != napi_ok) { 388 | // Something went wrong, so free the bytes we just allocated before 389 | // returning. 390 | roc_dealloc((void *)buf, __alignof__(char *)); 391 | 392 | return status; 393 | } 394 | 395 | *roc_bytes = init_roc_bytes(buf, len, capacity); 396 | 397 | return status; 398 | } 399 | 400 | // Consume the given RocStr (decrement its refcount) after creating a Node 401 | // string from it. 402 | napi_value roc_str_into_node_string(napi_env env, struct RocStr roc_str) { 403 | bool is_small = is_small_str(roc_str); 404 | char *roc_str_contents; 405 | 406 | if (is_small) { 407 | // In a small string, the string itself contains its contents. 408 | roc_str_contents = (char *)&roc_str; 409 | } else { 410 | roc_str_contents = (char *)roc_str.bytes; 411 | } 412 | 413 | napi_value answer; 414 | 415 | if (napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_str), 416 | &answer) != napi_ok) { 417 | answer = NULL; 418 | } 419 | 420 | // Decrement the RocStr because we consumed it. 421 | if (!is_small) { 422 | decref_large_str(roc_str); 423 | } 424 | 425 | return answer; 426 | } 427 | 428 | // Consume the given RocBytes (decrement its refcount) after creating a Node 429 | // string from it. (Assume we know these are UTF-8 bytes.) 430 | napi_value roc_bytes_into_node_string(napi_env env, struct RocBytes roc_bytes) { 431 | napi_value answer; 432 | 433 | if (napi_create_string_utf8(env, (char *)roc_bytes.bytes, roc_bytes.len, 434 | &answer) != napi_ok) { 435 | answer = NULL; 436 | } 437 | 438 | // Decrement the RocStr because we consumed it. 439 | decref_roc_bytes(roc_bytes); 440 | 441 | return answer; 442 | } 443 | 444 | // Create a Node string from the given RocStr. 445 | // Don't decrement the RocStr's refcount. (To decrement it, use 446 | // roc_str_into_node_string instead.) 447 | napi_value roc_str_as_node_string(napi_env env, struct RocStr roc_str) { 448 | bool is_small = is_small_str(roc_str); 449 | char *roc_str_contents; 450 | 451 | if (is_small) { 452 | // In a small string, the string itself contains its contents. 453 | roc_str_contents = (char *)&roc_str; 454 | } else { 455 | roc_str_contents = (char *)roc_str.bytes; 456 | } 457 | 458 | napi_status status; 459 | napi_value answer; 460 | 461 | status = napi_create_string_utf8(env, roc_str_contents, roc_str_len(roc_str), 462 | &answer); 463 | 464 | if (status != napi_ok) { 465 | return NULL; 466 | } 467 | 468 | // Do not decrement the RocStr's refcount because we did not consume it. 469 | 470 | return answer; 471 | } 472 | 473 | // Create a C string from the given RocStr. Don't reuse memory; do a fresh 474 | // malloc for it. Don't decrement the RocStr's refcount. (To decrement it, use 475 | // roc_str_into_c_string instead.) 476 | char *roc_str_into_c_string(struct RocStr roc_str) { 477 | char *roc_str_contents; 478 | size_t len; 479 | bool is_small = is_small_str(roc_str); 480 | 481 | if (is_small) { 482 | // In a small string, the string itself contains its contents. 483 | roc_str_contents = (char *)&roc_str; 484 | len = roc_str_len_small(roc_str); 485 | } else { 486 | roc_str_contents = (char *)roc_str.bytes; 487 | len = roc_str_len_big(roc_str); 488 | } 489 | 490 | char *buf = (char *)malloc(len + 1); // leave room for the \0 at the end 491 | 492 | // Copy the bytes from the string into the buffer 493 | memcpy(buf, roc_str_contents, len); 494 | 495 | // Write the \0 at the end 496 | buf[len] = '\0'; 497 | 498 | // Decrement the RocStr because we consumed it. 499 | if (!is_small) { 500 | decref_large_str(roc_str); 501 | } 502 | 503 | return buf; 504 | } 505 | 506 | void roc_panic(struct RocStr *roc_str) { 507 | last_signal = 0; 508 | last_roc_crash_msg = roc_str_into_c_string(*roc_str); 509 | 510 | longjmp(jump_on_crash, 1); 511 | } 512 | 513 | extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, 514 | struct RocBytes *arg); 515 | 516 | // Receive a string value from Node and pass it to Roc as a RocStr, then get a 517 | // RocStr back from Roc and convert it into a Node string. 518 | napi_value call_roc(napi_env env, napi_callback_info info) { 519 | // Set the jump point so we can recover from a segfault. 520 | if (setjmp(jump_on_crash) == 0) { 521 | // This is *not* the result of a longjmp 522 | 523 | // Get the argument passed to the Node function 524 | napi_value global, json, stringify, arg_buf[1], node_json_string; 525 | size_t argc = 1; 526 | napi_value argv[1]; 527 | 528 | napi_status status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL); 529 | 530 | if (status != napi_ok) { 531 | return NULL; 532 | } 533 | 534 | // Call JSON.stringify(node_arg) 535 | 536 | // Get the global object 537 | if (napi_get_global(env, &global) != napi_ok) { 538 | return NULL; 539 | } 540 | 541 | // global.JSON 542 | if (napi_get_named_property(env, global, "JSON", &json) != napi_ok) { 543 | return NULL; 544 | } 545 | 546 | // global.JSON.stringify 547 | if (napi_get_named_property(env, json, "stringify", &stringify) != 548 | napi_ok) { 549 | return NULL; 550 | } 551 | 552 | // Populate stringify's args 553 | napi_value node_arg = argv[0]; 554 | 555 | arg_buf[0] = node_arg; 556 | 557 | // Call JSON.stringify 558 | if (napi_call_function(env, json, stringify, 1, arg_buf, 559 | &node_json_string) != napi_ok) { 560 | return NULL; 561 | } 562 | 563 | // Translate the JSON string into a Roc List U8 564 | struct RocBytes roc_arg; 565 | 566 | if (node_string_into_roc_bytes(env, node_json_string, &roc_arg) != napi_ok) { 567 | return NULL; 568 | } 569 | 570 | struct RocBytes roc_ret; 571 | 572 | // Call the Roc function to populate `roc_ret`'s bytes. 573 | roc__mainForHost_1_exposed_generic(&roc_ret, &roc_arg); 574 | 575 | // Consume that List U8 to create the Node string. 576 | node_json_string = roc_bytes_into_node_string(env, roc_ret); 577 | 578 | napi_value parse; 579 | 580 | // JSON.parse 581 | if (napi_get_named_property(env, json, "parse", &parse) != napi_ok) { 582 | return NULL; 583 | } 584 | 585 | // Reuse the same arg_buf as last time 586 | arg_buf[0] = node_json_string; 587 | 588 | // Call JSON.parse on what we got back from Roc 589 | napi_value answer; 590 | 591 | if (napi_call_function(env, json, parse, 1, arg_buf, &answer) != napi_ok) { 592 | return NULL; 593 | } 594 | 595 | return answer; 596 | } else { 597 | // This *is* the result of a longjmp 598 | char *msg = last_roc_crash_msg != NULL ? (char *)last_roc_crash_msg 599 | : strsignal(last_signal); 600 | char *suffix = 601 | " while running `main` in a .roc file"; 602 | char *buf = 603 | malloc(strlen(msg) + strlen(suffix) + 1); // +1 for the null terminator 604 | 605 | strcpy(buf, msg); 606 | strcat(buf, suffix); 607 | 608 | napi_throw_error(env, NULL, buf); 609 | 610 | free(buf); 611 | 612 | return NULL; 613 | } 614 | } 615 | 616 | napi_value init(napi_env env, napi_value exports) { 617 | // Before doing anything else, install signal handlers in case subsequent C 618 | // code causes any of these. 619 | struct sigaction action; 620 | memset(&action, 0, sizeof(action)); 621 | action.sa_handler = signal_handler; 622 | 623 | // Handle all the signals that could take out the Node process and translate 624 | // them to exceptions. 625 | sigaction(SIGSEGV, &action, NULL); 626 | sigaction(SIGBUS, &action, NULL); 627 | sigaction(SIGFPE, &action, NULL); 628 | sigaction(SIGILL, &action, NULL); 629 | 630 | // Create our Node functions and expose them from this module. 631 | napi_status status; 632 | napi_value fn; 633 | 634 | status = napi_create_function(env, NULL, 0, call_roc, NULL, &fn); 635 | 636 | if (status != napi_ok) { 637 | return NULL; 638 | } 639 | 640 | status = napi_set_named_property(env, exports, "callRoc", fn); 641 | 642 | if (status != napi_ok) { 643 | return NULL; 644 | } 645 | 646 | return exports; 647 | } 648 | 649 | NAPI_MODULE(NODE_GYP_MODULE_NAME, init) 650 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Why this is a shell script: https://twitter.com/rtfeldman/status/1668776290021949444 4 | 5 | set -e 6 | 7 | os_name=$(uname) 8 | 9 | # Directory of the script itself 10 | script_dir=$(readlink -f "$(dirname "$0")") 11 | test_dir=$script_dir/tests 12 | 13 | function run { 14 | ($1 && printf "\n✅ Passed: %s\n" "$1") || { 15 | printf "\n❗️Failed: %s\n" "$1" 16 | exit 1 17 | } 18 | } 19 | 20 | if [ "$os_name" = "Linux" ]; then 21 | # We're running in Docker 22 | 23 | # First, run all the cross-compiled output code before rebuilding locally without cross-compilation 24 | for dir in "$test_dir"/*/ 25 | do 26 | # Don't treat node_modules as a test dir 27 | case "$dir" in 28 | *"node_modules"*) continue;; 29 | esac 30 | 31 | printf "\n⭐️ Running cross-compiled test: %s\n\n" "$dir" 32 | run "node $dir/dist/output.js" 33 | done 34 | 35 | # TODO this takes so long (presumably due to installing esbuild) on qemu-emulated Linux in Docker 36 | # that it's not worth it. 37 | 38 | # Now rebuild everything locally and try that (without cross-compilation). 39 | # echo "Running npm install to refresh roc-esbuild and tests" 40 | # npm install 41 | # cd "$test_dir" 42 | # npm install 43 | 44 | # echo "Running tests" 45 | 46 | # for dir in "$test_dir"/*/ 47 | # do 48 | # # Don't treat node_modules as a test dir 49 | # case "$dir" in 50 | # *"node_modules"*) continue;; 51 | # esac 52 | 53 | # run "rm -f $dir/*.d.ts" # These should get regenerated 54 | # printf "\n⭐️ Building and running test using roc-esbuild plugin: %s\n\n" "$dir" 55 | # run "node $test_dir/build.js" 56 | # run "$dir/dist/output.js" 57 | # done 58 | else 59 | # We're running in macOS, so cross-compile 60 | echo "Running npm install to refresh roc-esbuild and tests" 61 | rm -rf node_modules 62 | npm install 63 | cd "$test_dir" 64 | rm -rf node_modules 65 | npm install 66 | 67 | echo "Cross-compiling tests using zig cc..." 68 | 69 | for dir in "$test_dir"/*/ 70 | do 71 | # Don't treat node_modules as a test dir 72 | case "$dir" in 73 | *"node_modules"*) continue;; 74 | esac 75 | 76 | printf "\n⭐️ Cross-compiling test using roc-esbuild plugin with zig cc: %s\n\n" "$dir" 77 | run "node $test_dir/build.js $dir --cross-compile=linux-x64" 78 | run "npx tsc $dir/main.roc.d.ts" # Check that the generated .d.ts files worked 79 | done 80 | fi 81 | 82 | if [ "$os_name" != "Linux" ]; then 83 | printf "\nRunning tests in Docker to verify they pass on Linux\n\n" 84 | 85 | # Build the docker image, storing output in a tempfile and then printing it only if it failed. 86 | docker_image_name=roc-esbuild-tests 87 | 88 | docker build -t $docker_image_name "$test_dir" 89 | 90 | # Run the tests again in Docker 91 | # Specify --platform explicitly because Docker gives a warning if the one specified 92 | # in the Dockerfile is different from the one you're running on your system. 93 | docker run --platform=linux/amd64 -v "$script_dir:/app" $docker_image_name 94 | fi 95 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.c 3 | *.roc.d.ts 4 | *.js 5 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 haskell:9.2.2 2 | WORKDIR /app 3 | 4 | RUN apt-get update 5 | RUN apt-get -y install curl python3 strace 6 | 7 | # Install NVM 8 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash 9 | 10 | # Activate NVM 11 | ENV NVM_DIR=/root/.nvm 12 | RUN . "$NVM_DIR/nvm.sh" && nvm install 16.16.0 13 | 14 | # Add node and npm to path so the commands are available 15 | ENV NODE_PATH $NVM_DIR/versions/node/v16.16.0/lib/node_modules 16 | ENV PATH $NVM_DIR/versions/node/v16.16.0/bin:$PATH 17 | 18 | # Confirm installation 19 | RUN node -v 20 | RUN npm -v 21 | 22 | CMD bash test.sh 23 | -------------------------------------------------------------------------------- /tests/big-str/main.roc: -------------------------------------------------------------------------------- 1 | app "main" 2 | packages { pf: "platform/main.roc" } 3 | imports [] 4 | provides [main] to pf 5 | 6 | main : Str -> Str 7 | main = \message -> 8 | "TypeScript said to Roc: \(message)! 🎉" 9 | -------------------------------------------------------------------------------- /tests/big-str/platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "typescript-interop" 2 | requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } 3 | exposes [] 4 | packages {} 5 | imports [TotallyNotJson] 6 | provides [mainForHost] 7 | 8 | mainForHost : List U8 -> List U8 9 | mainForHost = \json -> 10 | when Decode.fromBytes json TotallyNotJson.json is 11 | Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json 12 | Err _ -> crash "Roc received malformed JSON from TypeScript" 13 | -------------------------------------------------------------------------------- /tests/big-str/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var main_roc_1 = require("./main.roc"); 4 | console.log("Roc says the following:", (0, main_roc_1.callRoc)("Hello from TypeScript")); 5 | -------------------------------------------------------------------------------- /tests/big-str/test.ts: -------------------------------------------------------------------------------- 1 | import { callRoc } from './main.roc' 2 | 3 | console.log("Roc says the following:", callRoc("Hello from TypeScript")); 4 | -------------------------------------------------------------------------------- /tests/build.js: -------------------------------------------------------------------------------- 1 | // Accepts a CLI arg for the directory of the test to run. 2 | const testDir = process.argv[2] 3 | 4 | // Accepts a CLI arg for whether to cross-compile 5 | const crossCompile = process.argv[3].startsWith("--cross-compile") ? process.argv[3].replace(/^--cross-compile=/, "") : undefined 6 | 7 | const path = require("path") 8 | const fs = require("fs") 9 | const esbuild = require("esbuild") 10 | const roc = require("roc-esbuild").default 11 | const { execSync } = require("child_process") 12 | 13 | const distDir = path.join(testDir, "dist") 14 | const outfile = path.join(distDir, "output.js") 15 | 16 | fs.rmSync(distDir, { recursive: true, force: true }); 17 | fs.mkdirSync(distDir) 18 | 19 | async function build() { 20 | const pluginArg = crossCompile ? { cc: ["zig", "cc"], target: crossCompile } : undefined; 21 | 22 | await esbuild 23 | .build({ 24 | entryPoints: [path.join(testDir, "test.ts")], 25 | bundle: true, 26 | outfile, 27 | sourcemap: "inline", 28 | platform: "node", 29 | minifyWhitespace: true, 30 | treeShaking: true, 31 | plugins: [roc(pluginArg)], 32 | }) 33 | .catch((err) => { 34 | console.error(err) 35 | process.exit(1) 36 | }); 37 | } 38 | 39 | build() 40 | -------------------------------------------------------------------------------- /tests/crash/main.roc: -------------------------------------------------------------------------------- 1 | app "main" 2 | packages { pf: "platform/main.roc" } 3 | imports [] 4 | provides [main] to pf 5 | 6 | main : Str -> Str 7 | main = \_message -> 8 | crash "This is an intentional crash!" 9 | -------------------------------------------------------------------------------- /tests/crash/platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "typescript-interop" 2 | requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } 3 | exposes [] 4 | packages {} 5 | imports [TotallyNotJson] 6 | provides [mainForHost] 7 | 8 | mainForHost : List U8 -> List U8 9 | mainForHost = \json -> 10 | when Decode.fromBytes json TotallyNotJson.json is 11 | Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json 12 | Err _ -> crash "Roc received malformed JSON from TypeScript" 13 | -------------------------------------------------------------------------------- /tests/crash/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var main_roc_1 = require("./main.roc"); 4 | try { 5 | (0, main_roc_1.callRoc)("Hello from TypeScript"); 6 | // We should not have reached this point! 7 | process.exit(1); 8 | } 9 | catch (err) { 10 | console.log("This is a test of Roc's error handling, and we successfully caught this error from Roc:", err); 11 | } 12 | -------------------------------------------------------------------------------- /tests/crash/test.ts: -------------------------------------------------------------------------------- 1 | import { callRoc } from './main.roc' 2 | 3 | try { 4 | callRoc("Hello from TypeScript") 5 | 6 | // We should not have reached this point! 7 | process.exit(1) 8 | } 9 | catch(err) { 10 | console.log("This is a test of Roc's error handling, and we successfully caught this error from Roc:", err); 11 | } 12 | -------------------------------------------------------------------------------- /tests/json/main.roc: -------------------------------------------------------------------------------- 1 | app "main" 2 | packages { pf: "platform/main.roc" } 3 | imports [] 4 | provides [main] to pf 5 | 6 | main : { firstName : Str, lastName : Str } -> Str 7 | main = \{ firstName, lastName } -> 8 | "TS says your first name is \(firstName) and your last name is \(lastName)! 🎉" 9 | -------------------------------------------------------------------------------- /tests/json/platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "typescript-interop" 2 | requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } 3 | exposes [] 4 | packages {} 5 | imports [TotallyNotJson] 6 | provides [mainForHost] 7 | 8 | mainForHost : List U8 -> List U8 9 | mainForHost = \json -> 10 | when Decode.fromBytes json TotallyNotJson.json is 11 | Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json 12 | Err _ -> crash "Roc received malformed JSON from TypeScript" 13 | -------------------------------------------------------------------------------- /tests/json/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var main_roc_1 = require("./main.roc"); 4 | console.log("Roc says the following:", (0, main_roc_1.callRoc)({ firstName: "Richard", lastName: "Feldman" })); 5 | -------------------------------------------------------------------------------- /tests/json/test.ts: -------------------------------------------------------------------------------- 1 | import { callRoc } from './main.roc' 2 | 3 | console.log("Roc says the following:", callRoc({ firstName: "Richard", lastName: "Feldman" })); 4 | -------------------------------------------------------------------------------- /tests/main-in-different-dir/platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "typescript-interop" 2 | requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } 3 | exposes [] 4 | packages {} 5 | imports [TotallyNotJson] 6 | provides [mainForHost] 7 | 8 | mainForHost : List U8 -> List U8 9 | mainForHost = \json -> 10 | when Decode.fromBytes json TotallyNotJson.json is 11 | Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json 12 | Err _ -> crash "Roc received malformed JSON from TypeScript" 13 | -------------------------------------------------------------------------------- /tests/main-in-different-dir/test.ts: -------------------------------------------------------------------------------- 1 | import { callRoc } from '../small-str/main.roc' 2 | 3 | console.log("I got this from ../small-str/main.roc:", callRoc("hi")); 4 | -------------------------------------------------------------------------------- /tests/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "example", 9 | "version": "1.0.0", 10 | "license": "UPL-1.0", 11 | "devDependencies": { 12 | "esbuild": "^0.17.19", 13 | "roc-esbuild": "file:.." 14 | } 15 | }, 16 | "..": { 17 | "version": "0.0.15", 18 | "dev": true, 19 | "hasInstallScript": true, 20 | "license": "UPL-1.0", 21 | "dependencies": { 22 | "roc-lang": "0.0.1-2023-06-21-nightly-buster" 23 | }, 24 | "devDependencies": { 25 | "@ava/typescript": "^4.0.0", 26 | "@types/eslint": "8.4.5", 27 | "@types/node": "^16.11.54", 28 | "@typescript-eslint/eslint-plugin": "5.38.0", 29 | "@typescript-eslint/parser": "5.38.0", 30 | "ava": "^5.3.0", 31 | "eslint": "8.24.0", 32 | "eslint-config-prettier": "8.5.0", 33 | "eslint-import-resolver-typescript": "3.5.1", 34 | "eslint-plugin-import": "2.26.0", 35 | "eslint-plugin-jsdoc": "39.3.6", 36 | "eslint-plugin-markdown": "3.0.0", 37 | "eslint-plugin-mocha": "10.1.0", 38 | "eslint-plugin-node": "11.1.0", 39 | "eslint-plugin-prettier": "4.2.1", 40 | "eslint-plugin-promise": "6.0.1", 41 | "eslint-plugin-tsdoc": "0.2.17", 42 | "ts-node": "^10.9.1", 43 | "typescript": "^5.1.3" 44 | }, 45 | "peerDependencies": { 46 | "esbuild": "^0.14.39" 47 | } 48 | }, 49 | "node_modules/@esbuild/android-arm": { 50 | "version": "0.17.19", 51 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 52 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 53 | "cpu": [ 54 | "arm" 55 | ], 56 | "dev": true, 57 | "optional": true, 58 | "os": [ 59 | "android" 60 | ], 61 | "engines": { 62 | "node": ">=12" 63 | } 64 | }, 65 | "node_modules/@esbuild/android-arm64": { 66 | "version": "0.17.19", 67 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 68 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 69 | "cpu": [ 70 | "arm64" 71 | ], 72 | "dev": true, 73 | "optional": true, 74 | "os": [ 75 | "android" 76 | ], 77 | "engines": { 78 | "node": ">=12" 79 | } 80 | }, 81 | "node_modules/@esbuild/android-x64": { 82 | "version": "0.17.19", 83 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 84 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 85 | "cpu": [ 86 | "x64" 87 | ], 88 | "dev": true, 89 | "optional": true, 90 | "os": [ 91 | "android" 92 | ], 93 | "engines": { 94 | "node": ">=12" 95 | } 96 | }, 97 | "node_modules/@esbuild/darwin-arm64": { 98 | "version": "0.17.19", 99 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 100 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 101 | "cpu": [ 102 | "arm64" 103 | ], 104 | "dev": true, 105 | "optional": true, 106 | "os": [ 107 | "darwin" 108 | ], 109 | "engines": { 110 | "node": ">=12" 111 | } 112 | }, 113 | "node_modules/@esbuild/darwin-x64": { 114 | "version": "0.17.19", 115 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 116 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 117 | "cpu": [ 118 | "x64" 119 | ], 120 | "dev": true, 121 | "optional": true, 122 | "os": [ 123 | "darwin" 124 | ], 125 | "engines": { 126 | "node": ">=12" 127 | } 128 | }, 129 | "node_modules/@esbuild/freebsd-arm64": { 130 | "version": "0.17.19", 131 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 132 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 133 | "cpu": [ 134 | "arm64" 135 | ], 136 | "dev": true, 137 | "optional": true, 138 | "os": [ 139 | "freebsd" 140 | ], 141 | "engines": { 142 | "node": ">=12" 143 | } 144 | }, 145 | "node_modules/@esbuild/freebsd-x64": { 146 | "version": "0.17.19", 147 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 148 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 149 | "cpu": [ 150 | "x64" 151 | ], 152 | "dev": true, 153 | "optional": true, 154 | "os": [ 155 | "freebsd" 156 | ], 157 | "engines": { 158 | "node": ">=12" 159 | } 160 | }, 161 | "node_modules/@esbuild/linux-arm": { 162 | "version": "0.17.19", 163 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 164 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 165 | "cpu": [ 166 | "arm" 167 | ], 168 | "dev": true, 169 | "optional": true, 170 | "os": [ 171 | "linux" 172 | ], 173 | "engines": { 174 | "node": ">=12" 175 | } 176 | }, 177 | "node_modules/@esbuild/linux-arm64": { 178 | "version": "0.17.19", 179 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 180 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 181 | "cpu": [ 182 | "arm64" 183 | ], 184 | "dev": true, 185 | "optional": true, 186 | "os": [ 187 | "linux" 188 | ], 189 | "engines": { 190 | "node": ">=12" 191 | } 192 | }, 193 | "node_modules/@esbuild/linux-ia32": { 194 | "version": "0.17.19", 195 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 196 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 197 | "cpu": [ 198 | "ia32" 199 | ], 200 | "dev": true, 201 | "optional": true, 202 | "os": [ 203 | "linux" 204 | ], 205 | "engines": { 206 | "node": ">=12" 207 | } 208 | }, 209 | "node_modules/@esbuild/linux-loong64": { 210 | "version": "0.17.19", 211 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 212 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 213 | "cpu": [ 214 | "loong64" 215 | ], 216 | "dev": true, 217 | "optional": true, 218 | "os": [ 219 | "linux" 220 | ], 221 | "engines": { 222 | "node": ">=12" 223 | } 224 | }, 225 | "node_modules/@esbuild/linux-mips64el": { 226 | "version": "0.17.19", 227 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 228 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 229 | "cpu": [ 230 | "mips64el" 231 | ], 232 | "dev": true, 233 | "optional": true, 234 | "os": [ 235 | "linux" 236 | ], 237 | "engines": { 238 | "node": ">=12" 239 | } 240 | }, 241 | "node_modules/@esbuild/linux-ppc64": { 242 | "version": "0.17.19", 243 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 244 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 245 | "cpu": [ 246 | "ppc64" 247 | ], 248 | "dev": true, 249 | "optional": true, 250 | "os": [ 251 | "linux" 252 | ], 253 | "engines": { 254 | "node": ">=12" 255 | } 256 | }, 257 | "node_modules/@esbuild/linux-riscv64": { 258 | "version": "0.17.19", 259 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 260 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 261 | "cpu": [ 262 | "riscv64" 263 | ], 264 | "dev": true, 265 | "optional": true, 266 | "os": [ 267 | "linux" 268 | ], 269 | "engines": { 270 | "node": ">=12" 271 | } 272 | }, 273 | "node_modules/@esbuild/linux-s390x": { 274 | "version": "0.17.19", 275 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 276 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 277 | "cpu": [ 278 | "s390x" 279 | ], 280 | "dev": true, 281 | "optional": true, 282 | "os": [ 283 | "linux" 284 | ], 285 | "engines": { 286 | "node": ">=12" 287 | } 288 | }, 289 | "node_modules/@esbuild/linux-x64": { 290 | "version": "0.17.19", 291 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 292 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 293 | "cpu": [ 294 | "x64" 295 | ], 296 | "dev": true, 297 | "optional": true, 298 | "os": [ 299 | "linux" 300 | ], 301 | "engines": { 302 | "node": ">=12" 303 | } 304 | }, 305 | "node_modules/@esbuild/netbsd-x64": { 306 | "version": "0.17.19", 307 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 308 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 309 | "cpu": [ 310 | "x64" 311 | ], 312 | "dev": true, 313 | "optional": true, 314 | "os": [ 315 | "netbsd" 316 | ], 317 | "engines": { 318 | "node": ">=12" 319 | } 320 | }, 321 | "node_modules/@esbuild/openbsd-x64": { 322 | "version": "0.17.19", 323 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 324 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 325 | "cpu": [ 326 | "x64" 327 | ], 328 | "dev": true, 329 | "optional": true, 330 | "os": [ 331 | "openbsd" 332 | ], 333 | "engines": { 334 | "node": ">=12" 335 | } 336 | }, 337 | "node_modules/@esbuild/sunos-x64": { 338 | "version": "0.17.19", 339 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 340 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 341 | "cpu": [ 342 | "x64" 343 | ], 344 | "dev": true, 345 | "optional": true, 346 | "os": [ 347 | "sunos" 348 | ], 349 | "engines": { 350 | "node": ">=12" 351 | } 352 | }, 353 | "node_modules/@esbuild/win32-arm64": { 354 | "version": "0.17.19", 355 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 356 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 357 | "cpu": [ 358 | "arm64" 359 | ], 360 | "dev": true, 361 | "optional": true, 362 | "os": [ 363 | "win32" 364 | ], 365 | "engines": { 366 | "node": ">=12" 367 | } 368 | }, 369 | "node_modules/@esbuild/win32-ia32": { 370 | "version": "0.17.19", 371 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 372 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 373 | "cpu": [ 374 | "ia32" 375 | ], 376 | "dev": true, 377 | "optional": true, 378 | "os": [ 379 | "win32" 380 | ], 381 | "engines": { 382 | "node": ">=12" 383 | } 384 | }, 385 | "node_modules/@esbuild/win32-x64": { 386 | "version": "0.17.19", 387 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 388 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 389 | "cpu": [ 390 | "x64" 391 | ], 392 | "dev": true, 393 | "optional": true, 394 | "os": [ 395 | "win32" 396 | ], 397 | "engines": { 398 | "node": ">=12" 399 | } 400 | }, 401 | "node_modules/esbuild": { 402 | "version": "0.17.19", 403 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 404 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 405 | "dev": true, 406 | "hasInstallScript": true, 407 | "bin": { 408 | "esbuild": "bin/esbuild" 409 | }, 410 | "engines": { 411 | "node": ">=12" 412 | }, 413 | "optionalDependencies": { 414 | "@esbuild/android-arm": "0.17.19", 415 | "@esbuild/android-arm64": "0.17.19", 416 | "@esbuild/android-x64": "0.17.19", 417 | "@esbuild/darwin-arm64": "0.17.19", 418 | "@esbuild/darwin-x64": "0.17.19", 419 | "@esbuild/freebsd-arm64": "0.17.19", 420 | "@esbuild/freebsd-x64": "0.17.19", 421 | "@esbuild/linux-arm": "0.17.19", 422 | "@esbuild/linux-arm64": "0.17.19", 423 | "@esbuild/linux-ia32": "0.17.19", 424 | "@esbuild/linux-loong64": "0.17.19", 425 | "@esbuild/linux-mips64el": "0.17.19", 426 | "@esbuild/linux-ppc64": "0.17.19", 427 | "@esbuild/linux-riscv64": "0.17.19", 428 | "@esbuild/linux-s390x": "0.17.19", 429 | "@esbuild/linux-x64": "0.17.19", 430 | "@esbuild/netbsd-x64": "0.17.19", 431 | "@esbuild/openbsd-x64": "0.17.19", 432 | "@esbuild/sunos-x64": "0.17.19", 433 | "@esbuild/win32-arm64": "0.17.19", 434 | "@esbuild/win32-ia32": "0.17.19", 435 | "@esbuild/win32-x64": "0.17.19" 436 | } 437 | }, 438 | "node_modules/roc-esbuild": { 439 | "resolved": "..", 440 | "link": true 441 | } 442 | }, 443 | "dependencies": { 444 | "@esbuild/android-arm": { 445 | "version": "0.17.19", 446 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 447 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 448 | "dev": true, 449 | "optional": true 450 | }, 451 | "@esbuild/android-arm64": { 452 | "version": "0.17.19", 453 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 454 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 455 | "dev": true, 456 | "optional": true 457 | }, 458 | "@esbuild/android-x64": { 459 | "version": "0.17.19", 460 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 461 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 462 | "dev": true, 463 | "optional": true 464 | }, 465 | "@esbuild/darwin-arm64": { 466 | "version": "0.17.19", 467 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 468 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 469 | "dev": true, 470 | "optional": true 471 | }, 472 | "@esbuild/darwin-x64": { 473 | "version": "0.17.19", 474 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 475 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 476 | "dev": true, 477 | "optional": true 478 | }, 479 | "@esbuild/freebsd-arm64": { 480 | "version": "0.17.19", 481 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 482 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 483 | "dev": true, 484 | "optional": true 485 | }, 486 | "@esbuild/freebsd-x64": { 487 | "version": "0.17.19", 488 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 489 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 490 | "dev": true, 491 | "optional": true 492 | }, 493 | "@esbuild/linux-arm": { 494 | "version": "0.17.19", 495 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 496 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 497 | "dev": true, 498 | "optional": true 499 | }, 500 | "@esbuild/linux-arm64": { 501 | "version": "0.17.19", 502 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 503 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 504 | "dev": true, 505 | "optional": true 506 | }, 507 | "@esbuild/linux-ia32": { 508 | "version": "0.17.19", 509 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 510 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 511 | "dev": true, 512 | "optional": true 513 | }, 514 | "@esbuild/linux-loong64": { 515 | "version": "0.17.19", 516 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 517 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 518 | "dev": true, 519 | "optional": true 520 | }, 521 | "@esbuild/linux-mips64el": { 522 | "version": "0.17.19", 523 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 524 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 525 | "dev": true, 526 | "optional": true 527 | }, 528 | "@esbuild/linux-ppc64": { 529 | "version": "0.17.19", 530 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 531 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 532 | "dev": true, 533 | "optional": true 534 | }, 535 | "@esbuild/linux-riscv64": { 536 | "version": "0.17.19", 537 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 538 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 539 | "dev": true, 540 | "optional": true 541 | }, 542 | "@esbuild/linux-s390x": { 543 | "version": "0.17.19", 544 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 545 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 546 | "dev": true, 547 | "optional": true 548 | }, 549 | "@esbuild/linux-x64": { 550 | "version": "0.17.19", 551 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 552 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 553 | "dev": true, 554 | "optional": true 555 | }, 556 | "@esbuild/netbsd-x64": { 557 | "version": "0.17.19", 558 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 559 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 560 | "dev": true, 561 | "optional": true 562 | }, 563 | "@esbuild/openbsd-x64": { 564 | "version": "0.17.19", 565 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 566 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 567 | "dev": true, 568 | "optional": true 569 | }, 570 | "@esbuild/sunos-x64": { 571 | "version": "0.17.19", 572 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 573 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 574 | "dev": true, 575 | "optional": true 576 | }, 577 | "@esbuild/win32-arm64": { 578 | "version": "0.17.19", 579 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 580 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 581 | "dev": true, 582 | "optional": true 583 | }, 584 | "@esbuild/win32-ia32": { 585 | "version": "0.17.19", 586 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 587 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 588 | "dev": true, 589 | "optional": true 590 | }, 591 | "@esbuild/win32-x64": { 592 | "version": "0.17.19", 593 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 594 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 595 | "dev": true, 596 | "optional": true 597 | }, 598 | "esbuild": { 599 | "version": "0.17.19", 600 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 601 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 602 | "dev": true, 603 | "requires": { 604 | "@esbuild/android-arm": "0.17.19", 605 | "@esbuild/android-arm64": "0.17.19", 606 | "@esbuild/android-x64": "0.17.19", 607 | "@esbuild/darwin-arm64": "0.17.19", 608 | "@esbuild/darwin-x64": "0.17.19", 609 | "@esbuild/freebsd-arm64": "0.17.19", 610 | "@esbuild/freebsd-x64": "0.17.19", 611 | "@esbuild/linux-arm": "0.17.19", 612 | "@esbuild/linux-arm64": "0.17.19", 613 | "@esbuild/linux-ia32": "0.17.19", 614 | "@esbuild/linux-loong64": "0.17.19", 615 | "@esbuild/linux-mips64el": "0.17.19", 616 | "@esbuild/linux-ppc64": "0.17.19", 617 | "@esbuild/linux-riscv64": "0.17.19", 618 | "@esbuild/linux-s390x": "0.17.19", 619 | "@esbuild/linux-x64": "0.17.19", 620 | "@esbuild/netbsd-x64": "0.17.19", 621 | "@esbuild/openbsd-x64": "0.17.19", 622 | "@esbuild/sunos-x64": "0.17.19", 623 | "@esbuild/win32-arm64": "0.17.19", 624 | "@esbuild/win32-ia32": "0.17.19", 625 | "@esbuild/win32-x64": "0.17.19" 626 | } 627 | }, 628 | "roc-esbuild": { 629 | "version": "file:..", 630 | "requires": { 631 | "@ava/typescript": "^4.0.0", 632 | "@types/eslint": "8.4.5", 633 | "@types/node": "^16.11.54", 634 | "@typescript-eslint/eslint-plugin": "5.38.0", 635 | "@typescript-eslint/parser": "5.38.0", 636 | "ava": "^5.3.0", 637 | "eslint": "8.24.0", 638 | "eslint-config-prettier": "8.5.0", 639 | "eslint-import-resolver-typescript": "3.5.1", 640 | "eslint-plugin-import": "2.26.0", 641 | "eslint-plugin-jsdoc": "39.3.6", 642 | "eslint-plugin-markdown": "3.0.0", 643 | "eslint-plugin-mocha": "10.1.0", 644 | "eslint-plugin-node": "11.1.0", 645 | "eslint-plugin-prettier": "4.2.1", 646 | "eslint-plugin-promise": "6.0.1", 647 | "eslint-plugin-tsdoc": "0.2.17", 648 | "roc-lang": "0.0.1-2023-06-21-nightly-buster", 649 | "ts-node": "^10.9.1", 650 | "typescript": "^5.1.3" 651 | } 652 | } 653 | } 654 | } 655 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "An example of using the roc-esbuild plgin", 5 | "main": "example.js", 6 | "scripts": { 7 | "start": "node example.js" 8 | }, 9 | "keywords": [ 10 | "roc", 11 | "esbuild" 12 | ], 13 | "author": "Richard Feldman", 14 | "license": "UPL-1.0", 15 | "devDependencies": { 16 | "esbuild": "0.14.39", 17 | "roc-esbuild": "file:.." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/small-str/main.roc: -------------------------------------------------------------------------------- 1 | app "main" 2 | packages { pf: "platform/main.roc" } 3 | imports [] 4 | provides [main] to pf 5 | 6 | main : Str -> Str 7 | main = \message -> 8 | "TypeScript said to Roc: \(message)! 🎉" 9 | -------------------------------------------------------------------------------- /tests/small-str/platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "typescript-interop" 2 | requires {} { main : arg -> ret where arg implements Decoding, ret implements Encoding } 3 | exposes [] 4 | packages {} 5 | imports [TotallyNotJson] 6 | provides [mainForHost] 7 | 8 | mainForHost : List U8 -> List U8 9 | mainForHost = \json -> 10 | when Decode.fromBytes json TotallyNotJson.json is 11 | Ok arg -> Encode.toBytes (main arg) TotallyNotJson.json 12 | Err _ -> crash "Roc received malformed JSON from TypeScript" 13 | -------------------------------------------------------------------------------- /tests/small-str/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var main_roc_1 = require("./main.roc"); 4 | console.log("Roc says the following:", (0, main_roc_1.callRoc)("hi")); 5 | -------------------------------------------------------------------------------- /tests/small-str/test.ts: -------------------------------------------------------------------------------- 1 | import { callRoc } from './main.roc' 2 | 3 | console.log("Roc says the following:", callRoc("hi")); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist2", 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "include": ["src/**/*.ts", "src/**/*.c"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /vendor/glue-platform/File.roc: -------------------------------------------------------------------------------- 1 | interface File 2 | exposes [File] 3 | imports [] 4 | 5 | File : { name : Str, content : Str } 6 | -------------------------------------------------------------------------------- /vendor/glue-platform/InternalTypeId.roc: -------------------------------------------------------------------------------- 1 | interface InternalTypeId 2 | exposes [InternalTypeId, fromNat, toNat] 3 | imports [] 4 | 5 | InternalTypeId : Nat 6 | 7 | toNat : InternalTypeId -> Nat 8 | toNat = \x -> x 9 | 10 | fromNat : Nat -> InternalTypeId 11 | fromNat = \x -> x 12 | -------------------------------------------------------------------------------- /vendor/glue-platform/Shape.roc: -------------------------------------------------------------------------------- 1 | interface Shape 2 | exposes [Shape, RocNum, RocTagUnion, RocStructFields, RocFn, RocSingleTagPayload] 3 | imports [TypeId.{ TypeId }] 4 | 5 | Shape : [ 6 | RocStr, 7 | Bool, 8 | RocResult TypeId TypeId, 9 | Num RocNum, 10 | RocList TypeId, 11 | RocDict TypeId TypeId, 12 | RocSet TypeId, 13 | RocBox TypeId, 14 | TagUnion RocTagUnion, 15 | EmptyTagUnion, 16 | Struct 17 | { 18 | name : Str, 19 | fields : RocStructFields, 20 | }, 21 | TagUnionPayload 22 | { 23 | name : Str, 24 | fields : RocStructFields, 25 | }, 26 | ## A recursive pointer, e.g. in StrConsList : [Nil, Cons Str StrConsList], 27 | ## this would be the field of Cons containing the (recursive) StrConsList type, 28 | ## and the TypeId is the TypeId of StrConsList itself. 29 | RecursivePointer TypeId, 30 | Function RocFn, 31 | # A zero-sized type, such as an empty record or a single-tag union with no payload 32 | Unit, 33 | Unsized, 34 | ] 35 | 36 | RocNum : [ 37 | I8, 38 | U8, 39 | I16, 40 | U16, 41 | I32, 42 | U32, 43 | I64, 44 | U64, 45 | I128, 46 | U128, 47 | F32, 48 | F64, 49 | Dec, 50 | ] 51 | 52 | RocTagUnion : [ 53 | Enumeration 54 | { 55 | name : Str, 56 | tags : List Str, 57 | size : U32, 58 | }, 59 | ## A non-recursive tag union 60 | ## e.g. `Result a e : [Ok a, Err e]` 61 | NonRecursive 62 | { 63 | name : Str, 64 | tags : List { name : Str, payload : [Some TypeId, None] }, 65 | discriminantSize : U32, 66 | discriminantOffset : U32, 67 | }, 68 | ## A recursive tag union (general case) 69 | ## e.g. `Expr : [Sym Str, Add Expr Expr]` 70 | Recursive 71 | { 72 | name : Str, 73 | tags : List { name : Str, payload : [Some TypeId, None] }, 74 | discriminantSize : U32, 75 | discriminantOffset : U32, 76 | }, 77 | ## A recursive tag union that has an empty variant 78 | ## Optimization: Represent the empty variant as null pointer => no memory usage & fast comparison 79 | ## It has more than one other variant, so they need tag IDs (payloads are "wrapped") 80 | ## e.g. `FingerTree a : [Empty, Single a, More (Some a) (FingerTree (Tuple a)) (Some a)]` 81 | ## see also: https://youtu.be/ip92VMpf_-A?t=164 82 | NullableWrapped 83 | { 84 | name : Str, 85 | indexOfNullTag : U16, 86 | tags : List { name : Str, payload : [Some TypeId, None] }, 87 | discriminantSize : U32, 88 | discriminantOffset : U32, 89 | }, 90 | ## Optimization: No need to store a tag ID (the payload is "unwrapped") 91 | ## e.g. `RoseTree a : [Tree a (List (RoseTree a))]` 92 | NonNullableUnwrapped 93 | { 94 | name : Str, 95 | tagName : Str, 96 | payload : TypeId, # These always have a payload. 97 | }, 98 | ## Optimization: No need to store a tag ID (the payload is "unwrapped") 99 | ## e.g. `[Foo Str Bool]` 100 | SingleTagStruct 101 | { 102 | name : Str, 103 | tagName : Str, 104 | payload : RocSingleTagPayload, 105 | }, 106 | ## A recursive tag union with only two variants, where one is empty. 107 | ## Optimizations: Use null for the empty variant AND don't store a tag ID for the other variant. 108 | ## e.g. `ConsList a : [Nil, Cons a (ConsList a)]` 109 | NullableUnwrapped 110 | { 111 | name : Str, 112 | nullTag : Str, 113 | nonNullTag : Str, 114 | nonNullPayload : TypeId, 115 | whichTagIsNull : [FirstTagIsNull, SecondTagIsNull], 116 | }, 117 | ] 118 | 119 | RocStructFields : [ 120 | HasNoClosure (List { name : Str, id : TypeId }), 121 | HasClosure (List { name : Str, id : TypeId, accessors : { getter : Str } }), 122 | ] 123 | 124 | RocSingleTagPayload : [ 125 | HasClosure (List { name : Str, id : TypeId }), 126 | HasNoClosure (List { id : TypeId }), 127 | ] 128 | 129 | RocFn : { 130 | functionName : Str, 131 | externName : Str, 132 | args : List TypeId, 133 | lambdaSet : TypeId, 134 | ret : TypeId, 135 | isToplevel : Bool, 136 | } 137 | -------------------------------------------------------------------------------- /vendor/glue-platform/Target.roc: -------------------------------------------------------------------------------- 1 | interface Target 2 | exposes [Target, Architecture, OperatingSystem] 3 | imports [] 4 | 5 | Target : { 6 | architecture : Architecture, 7 | operatingSystem : OperatingSystem, 8 | } 9 | 10 | Architecture : [ 11 | Aarch32, 12 | Aarch64, 13 | Wasm32, 14 | X86x32, 15 | X86x64, 16 | ] 17 | 18 | OperatingSystem : [ 19 | Windows, 20 | Unix, 21 | Wasi, 22 | ] 23 | -------------------------------------------------------------------------------- /vendor/glue-platform/TypeId.roc: -------------------------------------------------------------------------------- 1 | interface TypeId 2 | exposes [TypeId] 3 | imports [InternalTypeId.{ InternalTypeId }] 4 | 5 | TypeId : InternalTypeId 6 | -------------------------------------------------------------------------------- /vendor/glue-platform/Types.roc: -------------------------------------------------------------------------------- 1 | interface Types 2 | exposes [Types, shape, size, alignment, target, walkShapes, entryPoints] 3 | imports [Shape.{ Shape }, TypeId.{ TypeId }, Target.{ Target }, InternalTypeId] 4 | 5 | # TODO: switch AssocList uses to Dict once roc_std is updated. 6 | Tuple1 : [T Str TypeId] 7 | Tuple2 : [T TypeId (List TypeId)] 8 | 9 | Types := { 10 | # These are all indexed by TypeId 11 | types : List Shape, 12 | sizes : List U32, 13 | aligns : List U32, 14 | 15 | # Needed to check for duplicates 16 | typesByName : List Tuple1, 17 | 18 | ## Dependencies - that is, which type depends on which other type. 19 | ## This is important for declaration order in C; we need to output a 20 | ## type declaration earlier in the file than where it gets referenced by another type. 21 | deps : List Tuple2, 22 | 23 | ## Names and types of the entry points of the program (e.g. mainForHost) 24 | entrypoints : List Tuple1, 25 | target : Target, 26 | } 27 | 28 | target : Types -> Target 29 | target = \@Types types -> types.target 30 | 31 | entryPoints : Types -> List Tuple1 32 | entryPoints = \@Types { entrypoints } -> entrypoints 33 | 34 | walkShapes : Types, state, (state, Shape, TypeId -> state) -> state 35 | walkShapes = \@Types { types: shapes }, originalState, update -> 36 | List.walk shapes { index: 0, state: originalState } \{ index, state }, elem -> 37 | id = InternalTypeId.fromNat index 38 | 39 | { index: index + 1, state: update state elem id } 40 | |> .state 41 | 42 | shape : Types, TypeId -> Shape 43 | shape = \@Types types, id -> 44 | when List.get types.types (InternalTypeId.toNat id) is 45 | Ok answer -> answer 46 | Err OutOfBounds -> 47 | idStr = Num.toStr (InternalTypeId.toNat id) 48 | 49 | crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at " 50 | 51 | alignment : Types, TypeId -> U32 52 | alignment = \@Types types, id -> 53 | when List.get types.aligns (InternalTypeId.toNat id) is 54 | Ok answer -> answer 55 | Err OutOfBounds -> 56 | idStr = Num.toStr (InternalTypeId.toNat id) 57 | 58 | crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at " 59 | 60 | size : Types, TypeId -> U32 61 | size = \@Types types, id -> 62 | when List.get types.sizes (InternalTypeId.toNat id) is 63 | Ok answer -> answer 64 | Err OutOfBounds -> 65 | idStr = Num.toStr (InternalTypeId.toNat id) 66 | 67 | crash "TypeId #\(idStr) was not found in Types. This should never happen, and means there was a bug in `roc glue`. If you have time, please open an issue at " 68 | -------------------------------------------------------------------------------- /vendor/glue-platform/main.roc: -------------------------------------------------------------------------------- 1 | platform "roc-lang/glue" 2 | requires {} { makeGlue : List Types -> Result (List File) Str } 3 | exposes [Shape, File, Types, TypeId, Target] 4 | packages {} 5 | imports [Types.{ Types }, File.{ File }] 6 | provides [makeGlueForHost] 7 | 8 | makeGlueForHost : List Types -> Result (List File) Str 9 | makeGlueForHost = \types -> makeGlue types 10 | --------------------------------------------------------------------------------