├── core ├── testing │ ├── data │ │ ├── test_file.txt │ │ ├── no_permission.txt │ │ └── wc_input.txt │ ├── path_example │ │ ├── dir2 │ │ │ ├── file2.txt │ │ │ ├── dir21 │ │ │ │ └── file21.txt │ │ │ └── dir22 │ │ │ │ └── file22.txt │ │ └── dir1 │ │ │ └── dir11 │ │ │ └── file11.txt │ ├── rad_scripts │ │ ├── invalid.rad │ │ ├── example_arg.rad │ │ ├── debug.rad │ │ ├── print.rad │ │ ├── hello.rad │ │ ├── unknown_functions.rad │ │ ├── people_resource.rad │ │ ├── unknown_command_callbacks.rad │ │ └── wc.rad │ ├── rad_test_home │ │ └── stashes │ │ │ ├── with_stash │ │ │ ├── files │ │ │ │ └── existing.txt │ │ │ └── state.json │ │ │ └── save_state_test │ │ │ └── state.json │ ├── responses │ │ ├── root_prim_array.json │ │ ├── text.txt │ │ ├── not_root_array.json │ │ ├── id_name.json │ │ ├── numbers.json │ │ ├── array_wildcard.json │ │ ├── array_and_non_array.json │ │ ├── arrays.json │ │ ├── unique_keys.json │ │ ├── deeply_nested_arrays.json │ │ ├── obj_arr_with_arrays.json │ │ ├── parallel_arrays.json │ │ ├── unique_keys_array.json │ │ ├── people.json │ │ ├── array_objects.json │ │ ├── nested_wildcard.json │ │ ├── lots_of_types.json │ │ └── long_values.json │ ├── resources │ │ ├── mock_ages.json │ │ ├── website.json │ │ ├── people.json │ │ └── websites.json │ ├── func_get_args_test.go │ ├── func_upper_lower_test.go │ ├── func_get_default_test.go │ ├── mocking_test.go │ ├── func_misc_test.go │ ├── func_http_get_test.go │ ├── func_http_post_test.go │ ├── func_trim_test.go │ ├── string_test.go │ ├── func_trim_prefix_test.go │ ├── func_trim_suffix_test.go │ ├── pass_test.go │ ├── func_unique_test.go │ ├── func_sum_test.go │ ├── display_block_test.go │ ├── file_header_test.go │ ├── func_len_test.go │ ├── func_count_test.go │ ├── tbl_misc_test.go │ ├── exponential_nums_test.go │ ├── func_get_path_unix_test.go │ ├── str_escaping_test.go │ ├── func_reverse_test.go │ ├── map_dot_syntax_test.go │ ├── string_delimiters_test.go │ ├── flags_test.go │ ├── func_hash_test.go │ ├── func_ceil_test.go │ ├── underscore_nums_test.go │ ├── func_floor_test.go │ ├── func_keys_test.go │ ├── func_values_test.go │ ├── args_optional_test.go │ ├── expr_test.go │ ├── builtin_func_ref_test.go │ ├── numbers_test.go │ ├── func_get_env_test.go │ ├── func_replace_test.go │ ├── bool_test.go │ ├── func_clamp_test.go │ ├── func_split_test.go │ ├── while_test.go │ ├── revamp_test.go │ ├── slice_assign_test.go │ ├── func_str_test.go │ ├── constraint_enum_test.go │ ├── ufcs_test.go │ ├── func_parse_float_test.go │ ├── func_int_test.go │ ├── func_map_test.go │ ├── fallback_test.go │ ├── func_float_test.go │ ├── func_filter_test.go │ ├── tbl_format_test.go │ └── fn_return_yield_in_loop_test.go ├── embedded │ ├── home │ ├── gen-id │ ├── docs │ ├── stash │ ├── new │ └── check ├── version.go ├── type_null.go ├── args_helpers.go ├── funcs_stat_other.go ├── common │ ├── structs.go │ ├── struct_dump.go │ ├── utils_rad.go │ ├── terminal.go │ ├── stack.go │ └── colors.go ├── funcs_stat_windows.go ├── logging_rotation_windows.go ├── funcs_stat_linux.go ├── funcs_stat_darwin.go ├── consts.go ├── eval_ctx.go ├── func_exit.go ├── errors.go ├── args_mock.go ├── func_split.go ├── clock.go ├── rad_time.go ├── func_matches.go ├── type_error.go ├── func_rand.go ├── slicing.go ├── func_replace.go ├── defer.go ├── logging_rotation_unix.go ├── io.go ├── parsed_types.go ├── input.go ├── func_sleep.go ├── exit.go └── repl.go ├── docs-web ├── docs │ ├── examples │ │ └── hm.md │ ├── guide │ │ └── json-paths-advanced.md │ ├── reference │ │ ├── global-flags.md │ │ ├── json-field-definition.md │ │ ├── assignment.md │ │ ├── errors.md │ │ ├── rad-blocks.md │ │ ├── logic.md │ │ ├── args.md │ │ ├── math.md │ │ ├── shell-commands.md │ │ └── defer.md │ ├── stylesheets │ │ └── extra.css │ ├── assets │ │ ├── rad-color.png │ │ ├── vsc-example.png │ │ └── favicon.svg │ └── index.md └── README.md ├── assets ├── bash-example.png └── vsc-example.png ├── lsp-server ├── analysis │ ├── consts.go │ ├── diagnostics.go │ └── state_actions.go ├── com │ ├── consts.go │ ├── string.go │ └── errors.go ├── README.md ├── lsp │ ├── methods.go │ ├── error.go │ ├── init.go │ └── text_doc_diagnostics.go ├── Makefile ├── PACKAGES.md ├── main.go ├── log │ └── log.go └── rpc │ └── rpc.go ├── rts ├── .git-blame-ignore-revs ├── rl │ ├── rad_node.go │ └── util.go ├── name_transform.go ├── parse.go ├── README.md ├── .run │ └── Dump Tests.run.xml ├── test │ ├── dumps │ │ ├── cases │ │ │ ├── args_shorthand.dump │ │ │ ├── args_int_as_float_default.dump │ │ │ ├── args_rename.dump │ │ │ ├── args_repeat_unarys.dump │ │ │ ├── str_multiline_empty.dump │ │ │ ├── rad_req_no_url.dump │ │ │ ├── assign_multi.dump │ │ │ ├── str_multiline_simple.dump │ │ │ ├── str_multiline_indent.dump │ │ │ └── rad_map_identifier.dump │ │ └── test_dumps.rad │ └── rts_query_test.go ├── rts.go ├── name_transform_test.go ├── function_set.go └── embedded │ └── functions.txt ├── textmate-gen ├── main.go ├── go.mod ├── go.sum └── README.md ├── ci ├── benchmark-scripts │ ├── startup-time.rad │ ├── for-loop-add.rad │ ├── shell-commands.rad │ ├── string-operations.rad │ ├── file-io.rad │ ├── json-processing.rad │ └── display-table.rad ├── test-runner.rad ├── install-rad.sh └── README.md ├── vsc-extension ├── README.md ├── client │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── extension.ts ├── tsconfig.json ├── language-configuration.json └── dev ├── main.go ├── .idea ├── codeStyles │ └── codeStyleConfig.xml ├── vcs.xml ├── .gitignore ├── modules.xml ├── runConfigurations │ ├── make_all.xml │ ├── Rad.xml │ └── Tests.xml ├── rad.iml └── inspectionProfiles │ └── Project_Default.xml ├── docs ├── acknowledgements.md ├── thinking │ ├── principles.md │ ├── time.md │ ├── rad_check.md │ ├── resources.md │ ├── ufcs.md │ ├── rest_args.md │ ├── lsp.md │ └── arg_library.md ├── wiki │ └── switch stmts.md └── revisit.md ├── .git-blame-ignore-revs ├── .github └── workflows │ ├── homebrew.yml │ └── release.yml ├── benchmark ├── macos-hardware.rad ├── report.rad ├── README.md └── benchmark.rad ├── Makefile ├── function-metadata └── extract.go └── NAVIGATE.md /core/testing/data/test_file.txt: -------------------------------------------------------------------------------- 1 | hello bob -------------------------------------------------------------------------------- /core/testing/path_example/dir2/file2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/testing/data/no_permission.txt: -------------------------------------------------------------------------------- 1 | hello bob -------------------------------------------------------------------------------- /core/testing/path_example/dir1/dir11/file11.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/testing/path_example/dir2/dir21/file21.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/testing/path_example/dir2/dir22/file22.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/testing/rad_scripts/invalid.rad: -------------------------------------------------------------------------------- 1 | hello = 2 a 2 | if true: 3 | yes no -------------------------------------------------------------------------------- /docs-web/docs/examples/hm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: hm 3 | --- 4 | 5 | TBC 6 | -------------------------------------------------------------------------------- /docs-web/docs/guide/json-paths-advanced.md: -------------------------------------------------------------------------------- 1 | TBC (placeholder 2025-10-27) -------------------------------------------------------------------------------- /core/testing/rad_scripts/example_arg.rad: -------------------------------------------------------------------------------- 1 | args: 2 | name str # The name. -------------------------------------------------------------------------------- /core/testing/rad_test_home/stashes/with_stash/files/existing.txt: -------------------------------------------------------------------------------- 1 | hello there! -------------------------------------------------------------------------------- /core/testing/responses/root_prim_array.json: -------------------------------------------------------------------------------- 1 | [ 2 | 1, 3 | 2, 4 | 3 5 | ] -------------------------------------------------------------------------------- /core/testing/rad_scripts/debug.rad: -------------------------------------------------------------------------------- 1 | print("one") 2 | debug("two") 3 | debug("three") -------------------------------------------------------------------------------- /assets/bash-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amterp/rad/HEAD/assets/bash-example.png -------------------------------------------------------------------------------- /assets/vsc-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amterp/rad/HEAD/assets/vsc-example.png -------------------------------------------------------------------------------- /core/testing/rad_test_home/stashes/save_state_test/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "value" 3 | } -------------------------------------------------------------------------------- /core/testing/rad_test_home/stashes/with_stash/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "somekey": "somevalue" 3 | } -------------------------------------------------------------------------------- /core/testing/data/wc_input.txt: -------------------------------------------------------------------------------- 1 | The quick brown fox 2 | jumps over the lazy dog. 3 | Hello world! -------------------------------------------------------------------------------- /core/testing/rad_scripts/print.rad: -------------------------------------------------------------------------------- 1 | print("hi alice") 2 | print("hi bob") 3 | print("hi charlie") -------------------------------------------------------------------------------- /core/testing/resources/mock_ages.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"age": 30}, 3 | {"age": 10}, 4 | {"age": 20} 5 | ] 6 | -------------------------------------------------------------------------------- /core/testing/responses/text.txt: -------------------------------------------------------------------------------- 1 | This is just some text 2 | to emulate a non-structured 3 | response. woo! -------------------------------------------------------------------------------- /docs-web/docs/reference/global-flags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Global Flags 3 | --- 4 | 5 | !!! warning "WIP" 6 | -------------------------------------------------------------------------------- /docs-web/docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | .md-grid { 2 | /*max-width: 60%;*/ 3 | max-width: 68rem; 4 | }z -------------------------------------------------------------------------------- /core/testing/responses/not_root_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "names": ["Alice", "Bob", "Charlie"] 4 | } 5 | -------------------------------------------------------------------------------- /lsp-server/analysis/consts.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | const ( 4 | RadShebang = "#!/usr/bin/env rad" 5 | ) 6 | -------------------------------------------------------------------------------- /rts/.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Update dumps with comma removed 2 | 058125ffc1df35d7befc27b489bce348d8f704b0 3 | -------------------------------------------------------------------------------- /core/testing/rad_scripts/hello.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env radd 2 | args: 3 | name str 4 | 5 | print("Hello, {name}!") 6 | -------------------------------------------------------------------------------- /docs-web/docs/assets/rad-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amterp/rad/HEAD/docs-web/docs/assets/rad-color.png -------------------------------------------------------------------------------- /docs-web/docs/assets/vsc-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amterp/rad/HEAD/docs-web/docs/assets/vsc-example.png -------------------------------------------------------------------------------- /core/embedded/home: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Prints out rad's home directory. 4 | --- 5 | get_rad_home().print() 6 | -------------------------------------------------------------------------------- /core/version.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | const ( 4 | Version = "v0.6.25" 5 | // todo add in commit hash somehow? 6 | ) 7 | -------------------------------------------------------------------------------- /textmate-gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/amterp/rad/rts" 4 | 5 | func main() { 6 | rts.NewRts() 7 | } 8 | -------------------------------------------------------------------------------- /core/testing/rad_scripts/unknown_functions.rad: -------------------------------------------------------------------------------- 1 | foo() 2 | bar() 3 | qux() 4 | print() 5 | 6 | fn bar(): 7 | fn foo(): 8 | pass -------------------------------------------------------------------------------- /ci/benchmark-scripts/startup-time.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Measures startup time with minimal work. 4 | --- 5 | 6 | print("hello") -------------------------------------------------------------------------------- /lsp-server/com/consts.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | const ( 4 | RpcVersion = "2.0" 5 | RlsVersion = "0.0.1-0.4.33" // LsVersion-RadVersion 6 | ) 7 | -------------------------------------------------------------------------------- /vsc-extension/README.md: -------------------------------------------------------------------------------- 1 | # Rad Visual Studio Code Extension 2 | 3 | ## Dev 4 | 5 | Setup 6 | 7 | ```shell 8 | npm install 9 | ``` 10 | -------------------------------------------------------------------------------- /core/type_null.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type RadNull struct{} 4 | 5 | var RAD_NULL = RadNull{} 6 | var RAD_NULL_VAL = newRadValue(nil, nil, RAD_NULL) 7 | -------------------------------------------------------------------------------- /ci/benchmark-scripts/for-loop-add.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | args: 3 | iterations int = 50_000 4 | 5 | sum = 0 6 | for i in range(iterations): 7 | sum += i 8 | -------------------------------------------------------------------------------- /core/testing/responses/id_name.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Alice" 5 | }, 6 | { 7 | "id": 2, 8 | "name": "Bob" 9 | } 10 | ] -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/amterp/rad/core" 4 | 5 | func main() { 6 | runner := core.NewRadRunner(core.RunnerInput{}) 7 | runner.Run() 8 | } 9 | -------------------------------------------------------------------------------- /docs-web/docs/reference/json-field-definition.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Json Field Definition 3 | --- 4 | 5 | TODO 6 | perhaps reframe as general 'json extraction logic' reference? 7 | -------------------------------------------------------------------------------- /textmate-gen/go.mod: -------------------------------------------------------------------------------- 1 | module textmate-gen 2 | 3 | go 1.24.1 4 | 5 | require github.com/amterp/rts v0.0.1 // indirect 6 | 7 | // replace github.com/amterp/rts => ../../rts 8 | -------------------------------------------------------------------------------- /core/testing/rad_scripts/people_resource.rad: -------------------------------------------------------------------------------- 1 | args: 2 | filter str 3 | 4 | name, age = pick_from_resource("../resources/people.json", filter) 5 | print(name) 6 | print(age * 10) 7 | -------------------------------------------------------------------------------- /textmate-gen/go.sum: -------------------------------------------------------------------------------- 1 | github.com/amterp/rts v0.0.1 h1:VSCuVeNgF0wxmsLJduAf9UQoHW+bwOb/x9MC5Q/mVCI= 2 | github.com/amterp/rts v0.0.1/go.mod h1:I2MwI+RpkMShuH2FKFYnIZU4Gt3I229fdFIPJe69qiI= 3 | -------------------------------------------------------------------------------- /core/testing/responses/numbers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "shortint": 1, 4 | "longint": 1234567899987654321, 5 | "shortfloat": 1.12, 6 | "longfloat": 1234.567899987654321 7 | } 8 | ] -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /core/testing/responses/array_wildcard.json: -------------------------------------------------------------------------------- 1 | { 2 | "Alice": { 3 | "ids": [1, 2, 3] 4 | }, 5 | "Bob": { 6 | "ids": [4, 5, 6, 7, 8] 7 | }, 8 | "Charlie": { 9 | "ids": [9, 10] 10 | } 11 | } -------------------------------------------------------------------------------- /docs/acknowledgements.md: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | 3 | ## Inspiration / Examples To Learn From 4 | 5 | - Python 6 | - Go 7 | - [Amber Lang](https://github.com/amber-lang/amber) 8 | - [zx](https://github.com/google/zx) 9 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /core/testing/responses/array_and_non_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "len": 2, 3 | "results": [ 4 | { 5 | "name": "Alice", 6 | "age": 30 7 | }, 8 | { 9 | "name": "Bob", 10 | "age": 40 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /core/testing/resources/website.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": [ 3 | { 4 | "keys": ["gl"], 5 | "values": ["gitlab.com"] 6 | }, 7 | { 8 | "keys": ["gh"], 9 | "values": ["github.com"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /docs-web/README.md: -------------------------------------------------------------------------------- 1 | # Rad Docs 2 | 3 | Commands from the repo root. 4 | 5 | ## Local development 6 | 7 | ```sh 8 | mkdocs serve -f ./docs-web/mkdocs.yml 9 | ``` 10 | 11 | ## Deploy 12 | 13 | Use [deploy_docs.rad](../deploy_docs.rad) 14 | -------------------------------------------------------------------------------- /core/testing/resources/people.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": [ 3 | { 4 | "keys": ["alice"], 5 | "values": ["Alice", 25] 6 | }, 7 | { 8 | "keys": ["bob", "robert"], 9 | "values": ["Bob", 35] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /core/testing/responses/arrays.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Alice", 4 | "ids": [1, 2, 3] 5 | }, 6 | { 7 | "name": "Bob", 8 | "ids": [4, 5, 6, 7, 8] 9 | }, 10 | { 11 | "name": "Charlie", 12 | "ids": [9, 10] 13 | } 14 | ] -------------------------------------------------------------------------------- /core/testing/responses/unique_keys.json: -------------------------------------------------------------------------------- 1 | { 2 | "len": 2, 3 | "results": { 4 | "Alice": { 5 | "age": 30, 6 | "hometown": "New York" 7 | }, 8 | "Bob": { 9 | "age": 40, 10 | "hometown": "Los Angeles" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /core/args_helpers.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | func TransformRadArgs(args []RadArg, transformer func(RadArg) string) []string { 4 | output := make([]string, len(args)) 5 | for i, arg := range args { 6 | output[i] = transformer(arg) 7 | } 8 | return output 9 | } 10 | -------------------------------------------------------------------------------- /docs-web/docs/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 🤙 3 | -------------------------------------------------------------------------------- /core/funcs_stat_other.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !linux && !windows 2 | 3 | package core 4 | 5 | import "os" 6 | 7 | // getAccessTimeMillis is a fallback for unsupported Unix variants. 8 | func getAccessTimeMillis(fi os.FileInfo) (int64, bool) { 9 | return 0, false 10 | } 11 | -------------------------------------------------------------------------------- /core/testing/resources/websites.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": [ 3 | { 4 | "keys": ["gl", "lab"], 5 | "values": ["gitlab.com", "GitLab"] 6 | }, 7 | { 8 | "keys": ["gh", "hub"], 9 | "values": ["github.com", "GitHub"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ci/benchmark-scripts/shell-commands.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Measures shell command execution performance. 4 | --- 5 | args: 6 | iterations int = 150 7 | 8 | for i in range(iterations): 9 | if i % 2 == 0: 10 | quiet $`echo "test"` 11 | else: 12 | quiet $`pwd` -------------------------------------------------------------------------------- /core/testing/rad_scripts/unknown_command_callbacks.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | 3 | command first: 4 | calls missing_one 5 | 6 | command second: 7 | calls missing_two 8 | 9 | command third: 10 | calls print 11 | 12 | command fourth: 13 | calls fn(): 14 | print("inline") 15 | -------------------------------------------------------------------------------- /core/testing/responses/deeply_nested_arrays.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | [ 4 | [ 5 | 1, 2 6 | ], 7 | [ 8 | 3, 4 9 | ] 10 | ] 11 | ], 12 | [ 13 | [ 14 | [ 15 | 5, 6 16 | ], 17 | [ 18 | 7, 8 19 | ] 20 | ] 21 | ] 22 | ] -------------------------------------------------------------------------------- /rts/rl/rad_node.go: -------------------------------------------------------------------------------- 1 | package rl 2 | 3 | import ts "github.com/tree-sitter/go-tree-sitter" 4 | 5 | type RadNode struct { 6 | Node *ts.Node 7 | Src string 8 | } 9 | 10 | func NewRadNode(node *ts.Node, src string) *RadNode { 11 | return &RadNode{ 12 | Node: node, 13 | Src: src, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/common/structs.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | type Rgb struct { 4 | R int 5 | G int 6 | B int 7 | } 8 | 9 | func NewRgb(r, g, b int) Rgb { 10 | return Rgb{ 11 | R: r, 12 | G: g, 13 | B: b, 14 | } 15 | } 16 | 17 | func NewRgb64(r, g, b int64) Rgb { 18 | return NewRgb(int(r), int(g), int(b)) 19 | } 20 | -------------------------------------------------------------------------------- /vsc-extension/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": ["es2020"], 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", ".vscode-test"] 12 | } 13 | -------------------------------------------------------------------------------- /lsp-server/README.md: -------------------------------------------------------------------------------- 1 | # RSL LSP 2 | 3 | - **RLS**: **R**SL **L**anguage **S**erver 4 | 5 | ## Helpful Links 6 | 7 | - https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol 8 | - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ 9 | - https://www.youtube.com/watch?v=EkK8Jxjj95s 10 | -------------------------------------------------------------------------------- /core/common/struct_dump.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | import ( 4 | "github.com/sanity-io/litter" 5 | ) 6 | 7 | func Dump(item any) string { 8 | return litter.Sdump(item) 9 | } 10 | 11 | func init() { 12 | litter.Config.Compact = true 13 | litter.Config.StripPackageNames = true 14 | litter.Config.DisablePointerReplacement = true 15 | } 16 | -------------------------------------------------------------------------------- /lsp-server/com/string.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | import ( 4 | "github.com/sanity-io/litter" 5 | ) 6 | 7 | func FlatStr(item any) string { 8 | return litter.Sdump(item) 9 | } 10 | 11 | func init() { 12 | litter.Config.Compact = true 13 | litter.Config.StripPackageNames = true 14 | litter.Config.DisablePointerReplacement = true 15 | } 16 | -------------------------------------------------------------------------------- /rts/name_transform.go: -------------------------------------------------------------------------------- 1 | package rts 2 | 3 | import "strings" 4 | 5 | // ToExternalName converts internal argument names to external CLI flag names. 6 | // This is the single source of truth for name transformations in Rad. 7 | func ToExternalName(internalName string) string { 8 | return strings.Replace(internalName, "_", "-", -1) 9 | } 10 | -------------------------------------------------------------------------------- /core/funcs_stat_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package core 4 | 5 | import "os" 6 | 7 | // getAccessTimeMillis is a stub for Windows that returns false. 8 | // Access time is not easily available through the standard Go interface on Windows. 9 | func getAccessTimeMillis(fi os.FileInfo) (int64, bool) { 10 | return 0, false 11 | } 12 | -------------------------------------------------------------------------------- /core/testing/func_get_args_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Func_GetArgs(t *testing.T) { 6 | script := ` 7 | print(get_args()) 8 | ` 9 | setupAndRunCode(t, script, "--color=never") 10 | expected := `[ "--color=never" ] 11 | ` 12 | assertOnlyOutput(t, stdOutBuffer, expected) 13 | assertNoErrors(t) 14 | } 15 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # EBNF: Indent everything by one whitespace 2 | 0cac92dfa0ad447634e799565052a7c4fac1f8b4 3 | 4 | # EBNF: Add space to lines 5 | 7d0534b7b13a67794d2ab4f73c2d3b030381e288 6 | 7 | # Continue rsl -> Rad rename 8 | 8d069ed94f92a5d67c211d3f1cd2df91ddeebf7b 9 | 10 | # Continue RSL -> Rad rename 11 | 4d208ee285f981223289ed75a62aa8461a2cec63 12 | -------------------------------------------------------------------------------- /core/testing/responses/obj_arr_with_arrays.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Alice", 4 | "friends": [ 5 | { 6 | "name": "Bob" 7 | } 8 | ] 9 | }, 10 | { 11 | "name": "Bob", 12 | "friends": [ 13 | { 14 | "name": "Alice" 15 | }, 16 | { 17 | "name": "Charlie" 18 | } 19 | ] 20 | } 21 | ] -------------------------------------------------------------------------------- /core/testing/func_upper_lower_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_UpperLower(t *testing.T) { 6 | script := ` 7 | a = "aLiCe" 8 | print(upper(a)) 9 | print(lower(a))` 10 | setupAndRunCode(t, script, "--color=never") 11 | expected := `ALICE 12 | alice 13 | ` 14 | assertOnlyOutput(t, stdOutBuffer, expected) 15 | assertNoErrors(t) 16 | } 17 | -------------------------------------------------------------------------------- /core/testing/responses/parallel_arrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "people": { 3 | "country": "US", 4 | "names": [ 5 | "Alice", 6 | "Bob" 7 | ], 8 | "ages": [ 9 | 25, 10 | 30 11 | ] 12 | }, 13 | "dates": { 14 | "years": [ 15 | 2024, 16 | 2023 17 | ], 18 | "months": [ 19 | 12, 20 | 11 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /docs/thinking/principles.md: -------------------------------------------------------------------------------- 1 | # Principles 2 | 3 | ## RSL 4 | 5 | - RSL has a specific use case. Tailor to that. 6 | - Unless there's clear value in diverging from other programming language norms, then stick with what's familiar. 7 | - Err on the side of readability/verbosity. No cryptic symbols; RSL should be self-explanatory and comprehensible to people who've never seen it before. 8 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Update Homebrew Formula 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | homebrew: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: dawidd6/action-homebrew-bump-formula@v7 11 | with: 12 | token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} 13 | formula: rad 14 | tap: amterp/rad 15 | -------------------------------------------------------------------------------- /ci/test-runner.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Runs tests and dump tests for CI. Exits with proper codes. 4 | Dedicated CI script - independent from dev script. 5 | --- 6 | 7 | print("🧪 Running Go tests...") 8 | $`go test ./core/testing ./rts` 9 | 10 | print("🔍 Running dump tests...") 11 | $`cd ./rts/test/dumps && ./test_dumps.rad` 12 | 13 | print(green("✅ All tests passed!")) -------------------------------------------------------------------------------- /core/testing/responses/unique_keys_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "London": [ 3 | { 4 | "name": "Alice", 5 | "age": 30 6 | }, 7 | { 8 | "name": "Bob", 9 | "age": 40 10 | } 11 | ], 12 | "Paris": [ 13 | { 14 | "name": "Charlotte", 15 | "age": 35 16 | }, 17 | { 18 | "name": "David", 19 | "age": 25 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /core/logging_rotation_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package core 4 | 5 | // tryRotate is a stub for Windows that warns about unsupported rotation 6 | func tryRotate(config InvocationLoggingConfig, logPath string, maxBytes int64) { 7 | RP.RadStderrf("Warning! Log rotation not yet supported on Windows. Log file at %s has exceeded size limit. Please manually rotate.\n", logPath) 8 | } 9 | -------------------------------------------------------------------------------- /lsp-server/lsp/methods.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | const ( 4 | INITIALIZE = "initialize" 5 | TD_DID_OPEN = "textDocument/didOpen" 6 | TD_DID_CHANGE = "textDocument/didChange" 7 | TD_COMPLETION = "textDocument/completion" 8 | TD_CODE_ACTION = "textDocument/codeAction" 9 | TD_PUBLISH_DIAGNOSTICS = "textDocument/publishDiagnostics" 10 | ) 11 | -------------------------------------------------------------------------------- /vsc-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": ["es2020"], 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "sourceMap": true 9 | }, 10 | "include": [ 11 | "src" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | ".vscode-test" 16 | ], 17 | "references": [ 18 | { "path": "./client" }, 19 | ] 20 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/make_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /rts/parse.go: -------------------------------------------------------------------------------- 1 | package rts 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func ParseInt(src string) (int64, error) { 9 | toParse := strings.ReplaceAll(src, "_", "") 10 | return strconv.ParseInt(toParse, 10, 64) 11 | } 12 | 13 | func ParseFloat(src string) (float64, error) { 14 | toParse := strings.ReplaceAll(src, "_", "") 15 | return strconv.ParseFloat(toParse, 64) 16 | } 17 | -------------------------------------------------------------------------------- /core/testing/responses/people.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Charlie", 4 | "age": 30, 5 | "city": "Paris" 6 | }, 7 | { 8 | "name": "Bob", 9 | "age": 40, 10 | "city": "London" 11 | }, 12 | { 13 | "name": "Alice", 14 | "age": 30, 15 | "city": "New York" 16 | }, 17 | { 18 | "name": "Bob", 19 | "age": 25, 20 | "city": "Los Angeles" 21 | } 22 | ] -------------------------------------------------------------------------------- /lsp-server/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | # Directories 4 | BIN_DIR := ./bin 5 | 6 | # Commands 7 | GOFMT := gofmt -w 8 | GO := go 9 | 10 | .PHONY: all format build 11 | 12 | all: format build 13 | 14 | format: 15 | @echo "⚙️ Formatting code...." 16 | goimports -w . 17 | 18 | build: 19 | @echo "⚙️ Building the project..." 20 | mkdir -p $(BIN_DIR) 21 | $(GO) build -o $(BIN_DIR)/rls 22 | cp $(BIN_DIR)/rls ../vsc-extension/bin 23 | -------------------------------------------------------------------------------- /lsp-server/PACKAGES.md: -------------------------------------------------------------------------------- 1 | # Packages 2 | 3 | ```mermaid 4 | --- 5 | title: Packages 6 | --- 7 | flowchart TD 8 | 9 | com 10 | log 11 | 12 | lsp 13 | rpc 14 | server 15 | main 16 | analysis 17 | 18 | log-->server 19 | log-->main 20 | log-->rpc 21 | log-->analysis 22 | 23 | com-->lsp 24 | com-->rpc 25 | 26 | lsp-->|dependency of|server 27 | lsp-->rpc 28 | 29 | rpc-->server 30 | 31 | server-->main 32 | 33 | analysis-->server 34 | ``` 35 | -------------------------------------------------------------------------------- /.idea/rad.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /core/funcs_stat_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | 3 | package core 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | // getAccessTimeMillis extracts the access time from a FileInfo on Linux. 11 | func getAccessTimeMillis(fi os.FileInfo) (int64, bool) { 12 | if sys, ok := fi.Sys().(*syscall.Stat_t); ok { 13 | // Linux uses Atim 14 | millis := sys.Atim.Sec*1000 + sys.Atim.Nsec/1_000_000 15 | return millis, true 16 | } 17 | return 0, false 18 | } 19 | -------------------------------------------------------------------------------- /rts/rl/util.go: -------------------------------------------------------------------------------- 1 | package rl 2 | 3 | import ( 4 | ts "github.com/tree-sitter/go-tree-sitter" 5 | ) 6 | 7 | func GetChildren(node *ts.Node, fieldName string) []ts.Node { 8 | return node.ChildrenByFieldName(fieldName, node.Walk()) 9 | } 10 | 11 | func GetChild(node *ts.Node, fieldName string) *ts.Node { 12 | return node.ChildByFieldName(fieldName) 13 | } 14 | 15 | func GetSrc(node *ts.Node, src string) string { 16 | return src[node.StartByte():node.EndByte()] 17 | } 18 | -------------------------------------------------------------------------------- /core/funcs_stat_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package core 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | // getAccessTimeMillis extracts the access time from a FileInfo on macOS. 11 | func getAccessTimeMillis(fi os.FileInfo) (int64, bool) { 12 | if sys, ok := fi.Sys().(*syscall.Stat_t); ok { 13 | // macOS uses Atimespec 14 | millis := sys.Atimespec.Sec*1000 + sys.Atimespec.Nsec/1_000_000 15 | return millis, true 16 | } 17 | return 0, false 18 | } 19 | -------------------------------------------------------------------------------- /ci/benchmark-scripts/string-operations.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Measures string manipulation performance. 4 | --- 5 | args: 6 | iterations int = 20_000 7 | 8 | text = "hello world from rad scripting language" 9 | for i in range(iterations): 10 | upper_text = upper(text) 11 | lower_text = lower(text) 12 | parts = split(text, " ") 13 | joined = join(parts, "-") 14 | replaced = replace(text, "rad", "RAD") 15 | interpolated = "Result {i}: {replaced}" -------------------------------------------------------------------------------- /lsp-server/com/errors.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrFailedReadingHeader = errors.New("failed to read MIME header") 7 | ErrInvalidContentLengthHeader = errors.New("invalid Content-Length header") 8 | ErrFailedDecode = errors.New("failed to decode incoming request") 9 | ErrServerNotInitialized = errors.New("client did not initialize server") 10 | ErrMethodNotFound = errors.New("no handler for method found") 11 | ) 12 | -------------------------------------------------------------------------------- /ci/benchmark-scripts/file-io.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Measures file I/O performance. 4 | --- 5 | args: 6 | iterations int = 1000 7 | 8 | test_file = "/tmp/rad_benchmark_test.txt" 9 | content = "This is test content for the benchmark.\nIt has multiple lines.\nAnd some data to write." 10 | 11 | for i in range(iterations): 12 | write_file(test_file, "{content}\nIteration: {i}") 13 | result = read_file(test_file) 14 | text = result.content 15 | 16 | quiet $`rm -f {test_file}` -------------------------------------------------------------------------------- /core/common/utils_rad.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | func Truncate(str string, maxLen int64) string { 4 | if TerminalIsUtf8 { 5 | str = str[:maxLen-1] 6 | str += "…" 7 | } else { 8 | str = str[:maxLen-3] 9 | str += "..." 10 | } 11 | return str 12 | } 13 | 14 | func Reverse(str string) string { 15 | runeString := []rune(str) 16 | var reverseString string 17 | for i := len(runeString) - 1; i >= 0; i-- { 18 | reverseString += string(runeString[i]) 19 | } 20 | return reverseString 21 | } 22 | -------------------------------------------------------------------------------- /core/embedded/gen-id: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Generates a unique string ID. Useful for e.g. rad stash IDs. 4 | 5 | By default, generates FIDs: https://github.com/amterp/flexid 6 | --- 7 | args: 8 | uuid4 bool # Generate a uuid v4 instead of a FID. 9 | uuid7 bool # Generate a uuid v7 instead of a FID. 10 | 11 | uuid4 mutually excludes uuid7 12 | 13 | if uuid4: 14 | id = uuid_v4() 15 | else if uuid7: 16 | id = uuid_v7() 17 | else: 18 | id = gen_fid() 19 | 20 | id.print() 21 | -------------------------------------------------------------------------------- /core/testing/responses/array_objects.json: -------------------------------------------------------------------------------- 1 | { 2 | "Alice": { 3 | "ids": [ 4 | { 5 | "id": 1 6 | }, 7 | { 8 | "id": 2 9 | }, 10 | { 11 | "id": 3 12 | } 13 | ] 14 | }, 15 | "Bob": { 16 | "ids": [ 17 | { 18 | "id": 4 19 | } 20 | ] 21 | }, 22 | "Charlie": { 23 | "ids": [ 24 | { 25 | "id": 5 26 | }, 27 | { 28 | "id": 6 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /rts/README.md: -------------------------------------------------------------------------------- 1 | # RTS: RSL Tree Sitter 2 | 3 | A Go library wrapping the Go bindings for [Rad](https://github.com/amterp/rad)'s [tree sitter implementation](https://github.com/amterp/tree-sitter-rad). 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get -u github.com/amterp/rad/rts 9 | ``` 10 | 11 | # Git blame ignore revs 12 | 13 | This repo has a [`.git-blame-ignore-revs`](./.git-blame-ignore-revs) file. Add it to your git with: 14 | 15 | ```shell 16 | git config blame.ignoreRevsFile .git-blame-ignore-revs 17 | ``` 18 | -------------------------------------------------------------------------------- /core/testing/responses/nested_wildcard.json: -------------------------------------------------------------------------------- 1 | { 2 | "York": { 3 | "England": [ 4 | { 5 | "name": "Alice", 6 | "age": 30 7 | }, 8 | { 9 | "name": "Bob", 10 | "age": 40 11 | } 12 | ], 13 | "Australia": [ 14 | { 15 | "name": "Charlotte", 16 | "age": 35 17 | }, 18 | { 19 | "name": "David", 20 | "age": 25 21 | }, 22 | { 23 | "name": "Eve", 24 | "age": 20 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lsp-server/lsp/error.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func NewResponseError(id *json.RawMessage, err error) (resp Response) { 8 | return Response{ 9 | Msg: Msg{ 10 | Rpc: "2.0", 11 | }, 12 | Id: id, 13 | Result: nil, 14 | Error: newError(err), 15 | } 16 | } 17 | 18 | func newError(err error) *Error { 19 | if err != nil { 20 | return nil 21 | } 22 | return &Error{ 23 | Code: 0, // todo should probably not be 0 24 | Msg: err.Error(), 25 | Data: nil, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Rad.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /benchmark/macos-hardware.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | 3 | _, cpu = quiet $!`sysctl -n machdep.cpu.brand_string` 4 | cpu = replace(cpu, "\n", "") 5 | 6 | _, ram = quiet $!`sysctl -n hw.memsize` 7 | ram = replace(ram, "\n", "") 8 | ram = parse_int(ram) / 1024 / 1024 / 1024 9 | 10 | 11 | _, cores = quiet $!`sysctl -n hw.ncpu` 12 | cores = replace(cores, "\n", "") 13 | 14 | _, macos_vers = quiet $!`sw_vers -productVersion` 15 | macos_vers = replace(macos_vers, "\n", "") 16 | 17 | print("{cpu} ({cores} cores) {ram} GB macOS {macos_vers}") 18 | -------------------------------------------------------------------------------- /core/consts.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | const ( 4 | UNREACHABLE = "Bug! This should be unreachable" 5 | NOT_IMPLEMENTED = "not implemented" 6 | NO_NUM_RETURN_VALUES_CONSTRAINT = -1 7 | USAGE_ALIGNMENT_CHAR = "\x00" 8 | PADDING_CHAR = "\x00" 9 | ) 10 | 11 | const ( 12 | WILDCARD = "*" 13 | MACRO_STASH_ID = "stash_id" 14 | MACRO_ENABLE_GLOBAL_OPTIONS = "enable_global_options" 15 | MACRO_ENABLE_ARGS_BLOCK = "enable_args_block" 16 | ) 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /docs/thinking/time.md: -------------------------------------------------------------------------------- 1 | # Time 2 | 3 | ## Parsing Time 4 | 5 | In e.g. 1748858179 6 | 7 | 1. now(tz: string = ) -> map 8 | 2. parse_epoch(epoch: int, tz: string = ) -> map 9 | 10 | map looks like: 11 | 12 | ```json 13 | { 14 | "date": "2025-06-02", 15 | "day": 2, 16 | "epoch": { 17 | "millis": 1748858111281, 18 | "nanos": 1748858111281519000, 19 | "seconds": 1748858111 20 | }, 21 | "hour": 19, 22 | "minute": 55, 23 | "month": 6, 24 | "second": 11, 25 | "time": "19:55:11", 26 | "year": 2025 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /rts/.run/Dump Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs-web/docs/reference/assignment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Assignment 3 | --- 4 | 5 | Generally speaking, multi-assignments are only legal for switch expressions, or single operations (e.g. functions) that return multiple values. 6 | 7 | ## Legal Assignments 8 | 9 | ```rad 10 | a = 1 11 | a, b = pick_from_resoure(...) 12 | a, b = switch ... 13 | a, b = parse_int(text) 14 | 15 | myMap["key"] = 2 16 | myList[1] = 3 17 | ``` 18 | 19 | ## Illegal Assignments 20 | 21 | ```rad 22 | a, b = 1, 2 23 | myMap["key"], myMap["key2"] = 2, 3 24 | myList[1], myList[2] = 3, 4 25 | ``` 26 | -------------------------------------------------------------------------------- /core/common/terminal.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/mattn/go-isatty" 8 | ) 9 | 10 | var ( 11 | IsTty = checkTty() 12 | TerminalIsUtf8 = checkTerminalUtf8() 13 | ) 14 | 15 | func checkTty() bool { 16 | return isatty.IsTerminal(os.Stdout.Fd()) 17 | } 18 | 19 | func checkTerminalUtf8() bool { 20 | lang := os.Getenv("LANG") 21 | ctype := os.Getenv("LC_CTYPE") 22 | // Check for UTF-8 in LANG or LC_CTYPE environment variables 23 | return strings.Contains(lang, "UTF-8") || strings.Contains(ctype, "UTF-8") 24 | } 25 | -------------------------------------------------------------------------------- /core/testing/responses/lots_of_types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "name": "Alice", 5 | "old": true, 6 | "height": 1.7, 7 | "friends": [ 8 | { 9 | "id": 2, 10 | "name": "Bob" 11 | } 12 | ] 13 | }, 14 | { 15 | "id": 2, 16 | "name": "Bob", 17 | "old": false, 18 | "height": 1.8, 19 | "friends": [ 20 | { 21 | "id": 1, 22 | "name": "Alice" 23 | }, 24 | { 25 | "id": 3, 26 | "name": "Charlie", 27 | "height": null 28 | }, 29 | null 30 | ] 31 | }, 32 | null 33 | ] -------------------------------------------------------------------------------- /rts/test/dumps/cases/args_shorthand.dump: -------------------------------------------------------------------------------- 1 | ===== 2 | Args declarations 3 | ===== 4 | args: 5 | foo x str 6 | ===== 7 | B: [ 0, 19] PS: [0, 0] PE: [2, 0] source_file 8 | B: [ 0, 18] PS: [0, 0] PE: [1, 12] arg_block 9 | B: [ 0, 4] PS: [0, 0] PE: [0, 4] args `args` 10 | B: [ 4, 5] PS: [0, 4] PE: [0, 5] : `:` 11 | B: [ 9, 18] PS: [1, 3] PE: [1, 12] declaration: arg_declaration 12 | B: [ 9, 12] PS: [1, 3] PE: [1, 6] arg_name: identifier `foo` 13 | B: [13, 14] PS: [1, 7] PE: [1, 8] shorthand: shorthand_flag `x` 14 | B: [15, 18] PS: [1, 9] PE: [1, 12] type: string_type `str` 15 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for rad 2 | 3 | # Directories 4 | BIN_DIR := ./bin 5 | 6 | # Commands 7 | .PHONY: all format build test clean 8 | 9 | all: generate format build test 10 | 11 | generate: 12 | @echo "⚙️ Running generators..." 13 | go run "./function-metadata/extract.go" 14 | mv "./functions.txt" "./rts/embedded/" 15 | 16 | format: 17 | @echo "⚙️ Formatting files..." 18 | find . -name '*.go' -exec gofmt -w {} + 19 | goimports -w . 20 | 21 | build: 22 | @echo "⚙️ Building the project..." 23 | @mkdir -p $(BIN_DIR) 24 | go build -o $(BIN_DIR)/radd 25 | 26 | test: 27 | @echo "⚙️ Running tests..." 28 | go test ./core/testing ./rts 29 | -------------------------------------------------------------------------------- /vsc-extension/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rad-lsp-vsc-client", 3 | "description": "A Visual Studio Code client for the Rad language server.", 4 | "author": "Alexander Terp", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "publisher": "vscode", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/amterp/rad" 11 | }, 12 | "engines": { 13 | "vscode": "^1.75.0" 14 | }, 15 | "dependencies": { 16 | "glob": "^11.0.0", 17 | "vscode-languageclient": "^9.0.1" 18 | }, 19 | "devDependencies": { 20 | "@types/vscode": "^1.75.1", 21 | "@vscode/test-electron": "^2.3.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /function-metadata/extract.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/amterp/rad/core" 7 | 8 | "sort" 9 | ) 10 | 11 | func main() { 12 | functions := core.FunctionsByName 13 | 14 | names := make([]string, 0, len(functions)) 15 | for name := range functions { 16 | names = append(names, name) 17 | } 18 | 19 | sort.Strings(names) 20 | 21 | path := "functions.txt" 22 | 23 | file, err := os.Create(path) 24 | if err != nil { 25 | panic(err) 26 | } 27 | defer file.Close() 28 | 29 | for idx, name := range names { 30 | file.WriteString(name) 31 | if idx < len(names)-1 { 32 | file.WriteString("\n") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/testing/func_get_default_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Func_GetDefault_CanGet(t *testing.T) { 6 | script := ` 7 | m = { 1: "one", "two": 2 } 8 | v = m.get_default(1, "noo!") 9 | print(v) 10 | ` 11 | setupAndRunCode(t, script, "--color=never") 12 | assertOnlyOutput(t, stdOutBuffer, "one\n") 13 | assertNoErrors(t) 14 | } 15 | 16 | func Test_Func_GetDefault_DefaultsIfNotPresent(t *testing.T) { 17 | script := ` 18 | m = { 1: "one", "two": 2 } 19 | v = m.get_default(2, "noo!") 20 | print(v) 21 | ` 22 | setupAndRunCode(t, script, "--color=never") 23 | assertOnlyOutput(t, stdOutBuffer, "noo!\n") 24 | assertNoErrors(t) 25 | } 26 | -------------------------------------------------------------------------------- /docs-web/docs/reference/errors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Errors 3 | --- 4 | 5 | ## Codes 6 | 7 | ### RAD1xxxx - Syntax Errors 8 | 9 | None currently exist. 10 | 11 | ### RAD2xxxx - Runtime Errors 12 | 13 | #### RAD20001 14 | 15 | `parse_int` failed to parse the input. 16 | 17 | #### RAD20002 18 | 19 | `parse_float` failed to parse the input. 20 | 21 | #### RAD20003 22 | 23 | Failed to read the specified file. 24 | 25 | #### RAD20004 26 | 27 | Did not have permission to read the specified file. 28 | 29 | #### RAD20005 30 | 31 | Could not read the specified file, as it did not exist. 32 | 33 | 34 | #### RAD20006 35 | 36 | Failed to write to the specified file. 37 | -------------------------------------------------------------------------------- /docs-web/docs/reference/rad-blocks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rad Blocks 3 | --- 4 | 5 | ## `rad` block 6 | 7 | ```rad 8 | rad url: 9 | fields Name, Birthdate, Height 10 | Name: 11 | map fn(n) truncate(n, 20) 12 | if sort_by_height: 13 | sort Height, Name, Birthdate 14 | else: 15 | sort 16 | ``` 17 | 18 | ## `request` block 19 | 20 | ```rad 21 | request url: 22 | fields Name, Birthdate, Height 23 | ``` 24 | 25 | ## `display` block 26 | 27 | ```rad 28 | display: 29 | fields Name, Birthdate, Height 30 | ``` 31 | 32 | ## Colors 33 | 34 | Valid colors: 35 | 36 | `plain, black, red, green, yellow, blue, magenta, cyan, white, orange, pink` 37 | -------------------------------------------------------------------------------- /core/testing/responses/long_values.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "words": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis tristique mattis erat eget vulputate. Quisque fringilla finibus suscipit. Curabitur id scelerisque felis, pellentesque tempor elit. Pellentesque nec mauris diam. Cras et bibendum massa. Donec nisl neque, facilisis eu elit nec, lacinia aliquam risus. Curabitur sagittis non elit eget ultrices. Donec hendrerit enim ut ante efficitur bibendum." 5 | }, 6 | { 7 | "id": 2, 8 | "words": "Ut placerat magna vitae risus porta, eget laoreet libero fringilla. Mauris mi lectus, congue ac hendrerit in, dapibus sit amet elit. Fusce in iaculis erat. " 9 | } 10 | ] -------------------------------------------------------------------------------- /vsc-extension/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//" 4 | }, 5 | "brackets": [ 6 | ["{", "}"] 7 | ], 8 | "autoClosingPairs": [ 9 | { "open": "{", "close": "}" }, 10 | { "open": "[", "close": "]" }, 11 | { "open": "(", "close": ")" }, 12 | { "open": "\"", "close": "\"" }, 13 | { "open": "'", "close": "'" }, 14 | { "open": "`", "close": "`" } 15 | ], 16 | "surroundingPairs": [ 17 | { "open": "{", "close": "}" }, 18 | { "open": "[", "close": "]" }, 19 | { "open": "(", "close": ")" }, 20 | { "open": "\"", "close": "\"" }, 21 | { "open": "'", "close": "'" }, 22 | { "open": "`", "close": "`" } 23 | ] 24 | } -------------------------------------------------------------------------------- /ci/benchmark-scripts/json-processing.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Measures JSON parsing and serialization performance. 4 | --- 5 | args: 6 | iterations int = 5000 7 | 8 | json_str = r""" 9 | { 10 | "users": [ 11 | {"name": "alice", "age": 25, "email": "alice@example.com"}, 12 | {"name": "bob", "age": 30, "email": "bob@example.com"}, 13 | {"name": "charlie", "age": 35, "email": "charlie@example.com"} 14 | ], 15 | "metadata": { 16 | "version": "1.0", 17 | "timestamp": 1234567890 18 | } 19 | } 20 | """ 21 | 22 | for i in range(iterations): 23 | data = parse_json(json_str) 24 | serialized = str(data) 25 | name = data["users"][0]["name"] -------------------------------------------------------------------------------- /core/testing/mocking_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func TestMockResponse(t *testing.T) { 6 | script := ` 7 | url = "https://google.com" 8 | 9 | Id = json[].id 10 | Name = json[].name 11 | 12 | rad url: 13 | fields Id, Name 14 | ` 15 | 16 | setupAndRunCode(t, script, "--mock-response", ".*:./responses/id_name.json", "--color=never") 17 | // todo notice strange trailing whitespace in table below, would be good to trim probably 18 | expected := `Id Name 19 | 1 Alice 20 | 2 Bob 21 | ` 22 | assertOutput(t, stdOutBuffer, expected) 23 | assertOutput(t, stdErrBuffer, "Mocking response for url (matched \".*\"): https://google.com\n") 24 | assertNoErrors(t) 25 | } 26 | -------------------------------------------------------------------------------- /lsp-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/amterp/rad/lsp-server/log" 9 | "github.com/amterp/rad/lsp-server/server" 10 | ) 11 | 12 | var StdErr io.Writer = os.Stderr 13 | 14 | func main() { 15 | fmt.Fprintln(StdErr, "Spinning up Rad LSP server...") 16 | 17 | fmt.Fprintln(StdErr, "Initializing logger...") 18 | log.InitLogger(StdErr) 19 | log.L.Info("Logger initialized") 20 | 21 | log.L.Info("Creating server...") 22 | s := server.NewServer(os.Stdin, os.Stdout) 23 | 24 | log.L.Info("Running server...") 25 | err := s.Run() 26 | if err != nil { 27 | log.L.Fatalf("Error running server: %v", err) 28 | } 29 | log.L.Info("Exiting...") 30 | } 31 | -------------------------------------------------------------------------------- /core/testing/func_misc_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_StartsWith(t *testing.T) { 6 | script := ` 7 | a = "alice" 8 | print(starts_with(a, "al")) 9 | print(starts_with(a, "ce")) 10 | ` 11 | setupAndRunCode(t, script, "--color=never") 12 | expected := `true 13 | false 14 | ` 15 | assertOnlyOutput(t, stdOutBuffer, expected) 16 | assertNoErrors(t) 17 | } 18 | 19 | func Test_EndsWithWith(t *testing.T) { 20 | script := ` 21 | a = "alice" 22 | print(ends_with(a, "al")) 23 | print(ends_with(a, "ce")) 24 | ` 25 | setupAndRunCode(t, script, "--color=never") 26 | expected := `false 27 | true 28 | ` 29 | assertOnlyOutput(t, stdOutBuffer, expected) 30 | assertNoErrors(t) 31 | } 32 | -------------------------------------------------------------------------------- /docs-web/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome to Rad 3 | --- 4 | 5 | ## New? 6 | 7 | Check out the [Getting Started](./guide/getting-started.md) guide! 8 | 9 | ## Reference 10 | 11 | See 'Reference' in the side panel. 12 | 13 | !!! warning "These docs are still a work in progress!" 14 | 15 | These docs are actively being worked on, and there are critical sections missing. 16 | 17 | Rad is also evolving, and so some docs here may be out of date. If you have any questions at all, don't hesitate to ask on [Discussions](https://github.com/amterp/rad/discussions)! 18 | 19 | The [Guide](./guide/getting-started.md) is pretty substantial and gives you quite a lot to go on, feel free to check it out! 20 | -------------------------------------------------------------------------------- /rts/rts.go: -------------------------------------------------------------------------------- 1 | package rts 2 | 3 | import ( 4 | rad "github.com/amterp/tree-sitter-rad/bindings/go" 5 | ts "github.com/tree-sitter/go-tree-sitter" 6 | ) 7 | 8 | type RadParser struct { 9 | parser *ts.Parser 10 | } 11 | 12 | func NewRadParser() (rts *RadParser, err error) { 13 | parser := ts.NewParser() 14 | 15 | err = parser.SetLanguage(ts.NewLanguage(rad.Language())) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &RadParser{ 21 | parser: parser, 22 | }, nil 23 | } 24 | 25 | func (rts *RadParser) Close() { 26 | rts.parser.Close() 27 | } 28 | 29 | func (rts *RadParser) Parse(src string) *RadTree { 30 | tree := rts.parser.Parse([]byte(src), nil) 31 | return newRadTree(rts.parser, tree, src) 32 | } 33 | -------------------------------------------------------------------------------- /core/embedded/docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Opens rad's documentation website. 4 | 5 | Example: 6 | > rad docs functions get_path 7 | Opens the functions reference page on the 'get_path' function (via header). 8 | --- 9 | args: 10 | page str = "home" # The page to open on. 11 | header str = "" # An optional header to open to. 12 | 13 | page enum ["home", "functions"] 14 | 15 | url = `https://amterp.github.io/rad` 16 | 17 | // todo rad: convert to switch stmt 18 | if page == "home": 19 | // nothing to do 20 | else if page == "functions": 21 | url += "/reference/functions" 22 | 23 | if header: 24 | header = lower(header) 25 | url += "/#{header}" 26 | 27 | print("Opening {url}") 28 | quiet $`open {url}` 29 | -------------------------------------------------------------------------------- /rts/test/dumps/cases/args_int_as_float_default.dump: -------------------------------------------------------------------------------- 1 | ===== 2 | Args int as float default 3 | ===== 4 | args: 5 | floatArg float = 2 6 | ===== 7 | B: [ 0, 29] PS: [0, 0] PE: [2, 0] source_file 8 | B: [ 0, 28] PS: [0, 0] PE: [1, 22] arg_block 9 | B: [ 0, 4] PS: [0, 0] PE: [0, 4] args `args` 10 | B: [ 4, 5] PS: [0, 4] PE: [0, 5] : `:` 11 | B: [10, 28] PS: [1, 4] PE: [1, 22] declaration: arg_declaration 12 | B: [10, 18] PS: [1, 4] PE: [1, 12] arg_name: identifier `floatArg` 13 | B: [19, 24] PS: [1, 13] PE: [1, 18] type: float_type `float` 14 | B: [25, 26] PS: [1, 19] PE: [1, 20] = `=` 15 | B: [27, 28] PS: [1, 21] PE: [1, 22] default: float_arg 16 | B: [27, 28] PS: [1, 21] PE: [1, 22] value: int `2` 17 | -------------------------------------------------------------------------------- /core/eval_ctx.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "fmt" 4 | 5 | type ExpectedOutput int 6 | 7 | const ( 8 | Zero ExpectedOutput = iota 9 | One 10 | NoConstraint 11 | ) 12 | 13 | func (e ExpectedOutput) String() string { 14 | switch e { 15 | case Zero: 16 | return "no output" 17 | case One: 18 | return "1 output" 19 | case NoConstraint: 20 | return "output or no output" 21 | default: 22 | panic(fmt.Sprintf("Bug! Unhandled value: %d", e)) 23 | } 24 | } 25 | 26 | func (e ExpectedOutput) Acceptable(actual int) bool { 27 | switch e { 28 | case Zero: 29 | return actual == 0 30 | case One: 31 | return actual == 1 32 | case NoConstraint: 33 | return true 34 | default: 35 | panic(fmt.Sprintf("Bug! Unhandled value: %d", e)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/testing/func_http_get_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func TestHttpGet_Basic(t *testing.T) { 6 | script := ` 7 | url = "http//www.google.com" 8 | pprint(http_get(url)) 9 | ` 10 | setupAndRunCode(t, script, "--mock-response", ".*:./responses/id_name.json", "--color=never") 11 | expected := `{ 12 | "body": [ 13 | { 14 | "id": 1, 15 | "name": "Alice" 16 | }, 17 | { 18 | "id": 2, 19 | "name": "Bob" 20 | } 21 | ], 22 | "duration_seconds": 0, 23 | "status_code": 200, 24 | "success": true 25 | } 26 | ` 27 | assertOutput(t, stdOutBuffer, expected) 28 | assertOutput(t, stdErrBuffer, "Mocking response for url (matched \".*\"): http//www.google.com\n") 29 | assertNoErrors(t) 30 | } 31 | -------------------------------------------------------------------------------- /docs/wiki/switch stmts.md: -------------------------------------------------------------------------------- 1 | # switch stmts 2 | 3 | ## current plan 4 | 5 | ``` 6 | args: 7 | base string 8 | 9 | finalBase, title = switch base: 10 | case "github", "gh": "api.github", "Github" 11 | case "gitlab": "gitlab", "Gitlab" 12 | 13 | url = switch: 14 | case: "https://{finalBase}.com/repos/{repo}/commits?per_page={limit}" 15 | case: "https://{finalBase}.com/repos/{owner}/{project}/commits?per_page={limit}" 16 | ``` 17 | 18 | i.e. single-line cases 19 | 20 | ## eventually 21 | 22 | ``` 23 | finalBase, title = switch base: 24 | case "github", "gh": 25 | yield "api.github", "Github" 26 | case "gitlab": 27 | print("Jokes, Gitlab not supported yet!") 28 | yield "api.github", "Github" 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /core/testing/func_http_post_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func TestHttpPost_Basic(t *testing.T) { 6 | script := ` 7 | url = "http//www.google.com" 8 | pprint(http_post(url)) 9 | ` 10 | setupAndRunCode(t, script, "--mock-response", ".*:./responses/id_name.json", "--color=never") 11 | expected := `{ 12 | "body": [ 13 | { 14 | "id": 1, 15 | "name": "Alice" 16 | }, 17 | { 18 | "id": 2, 19 | "name": "Bob" 20 | } 21 | ], 22 | "duration_seconds": 0, 23 | "status_code": 200, 24 | "success": true 25 | } 26 | ` 27 | assertOutput(t, stdOutBuffer, expected) 28 | assertOutput(t, stdErrBuffer, "Mocking response for url (matched \".*\"): http//www.google.com\n") 29 | assertNoErrors(t) 30 | } 31 | -------------------------------------------------------------------------------- /benchmark/report.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Processes the hyperfine output report and prints a nice little report. 4 | --- 5 | 6 | Benchmark = json.results[].command 7 | Mean = json.results[].mean 8 | Stddev = json.results[].stddev 9 | Min = json.results[].min 10 | Max = json.results[].max 11 | Runs = json.results[].times 12 | 13 | // todo rad be able to pass own json blobs into rad blocks 14 | 15 | rad "mock-response!": 16 | fields Benchmark, Mean, Stddev, Min, Max, Runs 17 | sort Benchmark 18 | Benchmark: 19 | map fn(b) replace(split(b, "/")[-1], ".rad", "") 20 | Mean, Stddev, Min, Max: 21 | map fn(n) round(n * 1000, 1) 22 | Runs: 23 | map fn(t) len(t) 24 | print("Times in milliseconds") 25 | quiet $!`./macos-hardware.rad` 26 | -------------------------------------------------------------------------------- /core/testing/rad_scripts/wc.rad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Count lines, words, and characters from stdin 4 | --- 5 | args: 6 | lines l bool 7 | 8 | if not has_stdin(): 9 | print_err("wc: no input") 10 | exit(1) 11 | 12 | content = read_stdin() 13 | 14 | // Count lines 15 | line_list = content.split("\n") 16 | line_count = len(line_list) 17 | 18 | // If -l flag, only print line count 19 | if lines: 20 | print(line_count) 21 | exit(0) 22 | 23 | // Count words 24 | word_count = 0 25 | for line in line_list: 26 | trimmed = line.trim() 27 | if trimmed != "": 28 | words = trimmed.split(" ") 29 | word_count += len(words) 30 | 31 | // Character count (includes newlines) 32 | char_count = len(content) 33 | 34 | print("{line_count} {word_count} {char_count}") 35 | -------------------------------------------------------------------------------- /core/common/stack.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | type Stack[T any] struct { 4 | items []T 5 | } 6 | 7 | func NewStack[T any]() *Stack[T] { 8 | return &Stack[T]{} 9 | } 10 | 11 | func (s *Stack[T]) Push(item T) { 12 | s.items = append(s.items, item) 13 | } 14 | 15 | func (s *Stack[T]) Pop() (T, bool) { 16 | if len(s.items) == 0 { 17 | var zero T 18 | return zero, false 19 | } 20 | item := s.items[len(s.items)-1] 21 | s.items = s.items[:len(s.items)-1] 22 | return item, true 23 | } 24 | 25 | func (s *Stack[T]) Peek() (T, bool) { 26 | if len(s.items) == 0 { 27 | var zero T 28 | return zero, false 29 | } 30 | return s.items[len(s.items)-1], true 31 | } 32 | 33 | func (s *Stack[T]) Len() int { 34 | return len(s.items) 35 | } 36 | 37 | func (s *Stack[T]) IsEmpty() bool { 38 | return len(s.items) == 0 39 | } 40 | -------------------------------------------------------------------------------- /core/func_exit.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var FuncExit = BuiltInFunc{ 8 | Name: FUNC_EXIT, 9 | Execute: func(f FuncInvocation) RadValue { 10 | err := f.GetIntAllowingBool("_code") 11 | exit(f.i, err) 12 | return VOID_SENTINEL 13 | }, 14 | } 15 | 16 | func exit(i *Interpreter, errorCode int64) { 17 | if FlagShell.Value { 18 | if errorCode == 0 { 19 | RP.RadDebugf(fmt.Sprintf("Printing shell exports")) 20 | i.env.PrintShellExports() 21 | } else { 22 | // error scenario, we want the shell script to exit, so just print a shell exit to be eval'd 23 | RP.RadDebugf(fmt.Sprintf("Printing shell exit %d", errorCode)) 24 | RP.PrintForShellEval(fmt.Sprintf("exit %d\n", errorCode)) 25 | } 26 | } 27 | 28 | RP.RadDebugf("Exiting") 29 | RExit.Exit(int(errorCode)) 30 | } 31 | -------------------------------------------------------------------------------- /core/common/colors.go: -------------------------------------------------------------------------------- 1 | package com 2 | 3 | import "github.com/amterp/color" 4 | 5 | var ( 6 | plain = color.New(color.Reset) 7 | green = color.New(color.FgGreen) 8 | greenBold = color.New(color.FgGreen, color.Bold) 9 | yellow = color.New(color.FgYellow) 10 | cyan = color.New(color.FgCyan) 11 | bold = color.New(color.Bold) 12 | 13 | PlainF = plain.FprintfFunc() 14 | GreenF = green.FprintfFunc() 15 | GreenBoldF = greenBold.FprintfFunc() 16 | YellowF = yellow.FprintfFunc() 17 | CyanF = cyan.FprintfFunc() 18 | BoldF = bold.FprintfFunc() 19 | 20 | PlainS = plain.SprintfFunc() 21 | GreenS = green.SprintfFunc() 22 | GreenBoldS = greenBold.SprintfFunc() 23 | YellowS = yellow.SprintfFunc() 24 | CyanS = cyan.SprintfFunc() 25 | BoldS = bold.SprintfFunc() 26 | ) 27 | -------------------------------------------------------------------------------- /core/testing/func_trim_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Func_Trim_NoChars(t *testing.T) { 6 | script := ` 7 | a = " hello " 8 | print(trim(a)) 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "hello\n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func Test_Func_Trim_OneChar(t *testing.T) { 16 | script := ` 17 | a = ",,!!,hello,!," 18 | print(trim(a, ",")) 19 | ` 20 | setupAndRunCode(t, script, "--color=never") 21 | assertOnlyOutput(t, stdOutBuffer, "!!,hello,!\n") 22 | assertNoErrors(t) 23 | } 24 | 25 | func Test_Func_Trim_MultipleChar(t *testing.T) { 26 | script := ` 27 | a = ",,!!,hello,!," 28 | print(trim(a, "!,")) 29 | ` 30 | setupAndRunCode(t, script, "--color=never") 31 | assertOnlyOutput(t, stdOutBuffer, "hello\n") 32 | assertNoErrors(t) 33 | } 34 | -------------------------------------------------------------------------------- /core/testing/string_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func TestString_SimpleIndexing(t *testing.T) { 6 | script := ` 7 | a = "alice" 8 | print(a[0]) 9 | print(a[1]) 10 | ` 11 | setupAndRunCode(t, script, "--color=never") 12 | assertOnlyOutput(t, stdOutBuffer, "a\nl\n") 13 | assertNoErrors(t) 14 | } 15 | 16 | func TestString_NegativeIndexing(t *testing.T) { 17 | script := ` 18 | a = "alice" 19 | print(a[-1]) 20 | print(a[-2]) 21 | ` 22 | setupAndRunCode(t, script, "--color=never") 23 | assertOnlyOutput(t, stdOutBuffer, "e\nc\n") 24 | assertNoErrors(t) 25 | } 26 | 27 | func TestString_ComplexIndexing(t *testing.T) { 28 | script := ` 29 | a = "alice" 30 | print(a[len(a) - 3]) 31 | ` 32 | setupAndRunCode(t, script, "--color=never") 33 | assertOnlyOutput(t, stdOutBuffer, "i\n") 34 | assertNoErrors(t) 35 | } 36 | -------------------------------------------------------------------------------- /core/errors.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | ts "github.com/tree-sitter/go-tree-sitter" 5 | ) 6 | 7 | func ErrIndexOutOfBounds(i *Interpreter, node *ts.Node, idx int64, length int64) { 8 | i.errorf(node, "Index out of bounds: %d (length %d)", idx, length) 9 | } 10 | 11 | type RadPanic struct { 12 | ErrV RadValue 13 | ShellResult *shellResult // For shell command errors, contains exit code/stdout/stderr 14 | } 15 | 16 | func (i *Interpreter) NewRadPanic(node *ts.Node, err RadValue) *RadPanic { 17 | unwrapped := err.RequireError(i, node) 18 | if unwrapped.Node == nil { 19 | unwrapped.Node = node 20 | } 21 | return &RadPanic{ 22 | ErrV: err, 23 | } 24 | } 25 | 26 | func (p *RadPanic) Err() *RadError { 27 | err, _ := p.ErrV.Val.(*RadError) 28 | return err 29 | } 30 | 31 | func (p *RadPanic) Panic() { 32 | panic(p) 33 | } 34 | -------------------------------------------------------------------------------- /core/embedded/stash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rad 2 | --- 3 | Interacts with script stashes. 4 | --- 5 | args: 6 | script str # Which script's stash to interact with. 7 | delete bool # Enable to delete the state. 8 | id bool # Enable to print the stash ID. 9 | state bool # Enable to print the state. 10 | 11 | stash_id = _rad_get_stash_id(script) 12 | 13 | if id: 14 | print(stash_id) 15 | 16 | if not stash_id: 17 | print("Found no stash ID for script '{script}'.") 18 | exit(1) 19 | 20 | if delete: 21 | _rad_delete_stash(stash_id) 22 | exit() 23 | 24 | if state: 25 | state_path = `{get_rad_home()}/stashes/{stash_id}/state.json` 26 | path = get_path(state_path) 27 | if path.exists: 28 | path.full_path.read_file().content.parse_json().pprint() 29 | else: 30 | print("No state file for this stash ID.") 31 | -------------------------------------------------------------------------------- /core/args_mock.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type MockResponse struct { 9 | Pattern string 10 | FilePath string 11 | } 12 | 13 | type MockResponseSlice []MockResponse 14 | 15 | func (m *MockResponseSlice) String() string { 16 | var result []string 17 | for _, mock := range *m { 18 | result = append(result, fmt.Sprintf("%q %s", mock.Pattern, mock.FilePath)) 19 | } 20 | return strings.Join(result, ", ") 21 | } 22 | 23 | func (m *MockResponseSlice) Set(value string) error { 24 | index := strings.LastIndex(value, ":") 25 | 26 | if index == -1 { 27 | return fmt.Errorf("invalid format: expected pattern:filePath") 28 | } 29 | 30 | *m = append(*m, MockResponse{Pattern: value[:index], FilePath: value[index+1:]}) 31 | return nil 32 | } 33 | 34 | func (m *MockResponseSlice) Type() string { 35 | return "mockResponse" 36 | } 37 | -------------------------------------------------------------------------------- /core/testing/func_trim_prefix_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Func_Trim_Prefix_NoChars(t *testing.T) { 6 | script := ` 7 | a = " hello " 8 | print(trim_prefix(a)) 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "hello \n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func Test_Func_Trim_Prefix_OneChar(t *testing.T) { 16 | script := ` 17 | a = ",,!!,hello,!," 18 | print(trim_prefix(a, ",")) 19 | ` 20 | setupAndRunCode(t, script, "--color=never") 21 | assertOnlyOutput(t, stdOutBuffer, "!!,hello,!,\n") 22 | assertNoErrors(t) 23 | } 24 | 25 | func Test_Func_Trim_Prefix_MultipleChar(t *testing.T) { 26 | script := ` 27 | a = ",,!!,hello,!," 28 | print(trim_prefix(a, "!,")) 29 | ` 30 | setupAndRunCode(t, script, "--color=never") 31 | assertOnlyOutput(t, stdOutBuffer, "hello,!,\n") 32 | assertNoErrors(t) 33 | } 34 | -------------------------------------------------------------------------------- /rts/test/dumps/cases/args_rename.dump: -------------------------------------------------------------------------------- 1 | ===== 2 | Args declarations 3 | ===== 4 | args: 5 | foo "bar" str 6 | ===== 7 | B: [ 0, 23] PS: [0, 0] PE: [2, 0] source_file 8 | B: [ 0, 22] PS: [0, 0] PE: [1, 16] arg_block 9 | B: [ 0, 4] PS: [0, 0] PE: [0, 4] args `args` 10 | B: [ 4, 5] PS: [0, 4] PE: [0, 5] : `:` 11 | B: [ 9, 22] PS: [1, 3] PE: [1, 16] declaration: arg_declaration 12 | B: [ 9, 12] PS: [1, 3] PE: [1, 6] arg_name: identifier `foo` 13 | B: [13, 18] PS: [1, 7] PE: [1, 12] rename: string 14 | B: [13, 14] PS: [1, 7] PE: [1, 8] start: string_start `"` 15 | B: [14, 17] PS: [1, 8] PE: [1, 11] contents: string_contents 16 | B: [14, 17] PS: [1, 8] PE: [1, 11] content: string_content `bar` 17 | B: [17, 18] PS: [1, 11] PE: [1, 12] end: string_end `"` 18 | B: [19, 22] PS: [1, 13] PE: [1, 16] type: string_type `str` 19 | -------------------------------------------------------------------------------- /core/func_split.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | ts "github.com/tree-sitter/go-tree-sitter" 8 | ) 9 | 10 | var FuncSplit = BuiltInFunc{ 11 | Name: FUNC_SPLIT, 12 | Execute: func(f FuncInvocation) RadValue { 13 | toSplit := f.GetStr("_val").Plain() 14 | splitter := f.GetStr("_sep").Plain() 15 | 16 | return f.Return(regexSplit(f.i, f.callNode, toSplit, splitter)) 17 | }, 18 | } 19 | 20 | func regexSplit(i *Interpreter, callNode *ts.Node, input string, sep string) []RadValue { 21 | re, err := regexp.Compile(sep) 22 | 23 | var parts []string 24 | if err == nil { 25 | parts = re.Split(input, -1) 26 | } else { 27 | parts = strings.Split(input, sep) 28 | } 29 | 30 | result := make([]RadValue, 0, len(parts)) 31 | for _, part := range parts { 32 | result = append(result, newRadValue(i, callNode, part)) 33 | } 34 | 35 | return result 36 | } 37 | -------------------------------------------------------------------------------- /core/testing/func_trim_suffix_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Func_Trim_Suffix_NoChars(t *testing.T) { 6 | script := ` 7 | a = " hello " 8 | print(trim_suffix(a)) 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "\thello\n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func Test_Func_Trim_Suffix_OneChar(t *testing.T) { 16 | script := ` 17 | a = ",,!!,hello,!," 18 | print(trim_suffix(a, ",")) 19 | ` 20 | setupAndRunCode(t, script, "--color=never") 21 | assertOnlyOutput(t, stdOutBuffer, ",,!!,hello,!\n") 22 | assertNoErrors(t) 23 | } 24 | 25 | func Test_Func_Trim_Suffix_MultipleChar(t *testing.T) { 26 | script := ` 27 | a = ",,!!,hello,!," 28 | print(trim_suffix(a, "!,")) 29 | ` 30 | setupAndRunCode(t, script, "--color=never") 31 | assertOnlyOutput(t, stdOutBuffer, ",,!!,hello\n") 32 | assertNoErrors(t) 33 | } 34 | -------------------------------------------------------------------------------- /core/testing/pass_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Pass_Root(t *testing.T) { 6 | script := ` 7 | pass 8 | print("Made it!") 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "Made it!\n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func Test_Pass_IfStmt(t *testing.T) { 16 | script := ` 17 | if true: 18 | pass 19 | else: 20 | pass 21 | 22 | if false: 23 | pass 24 | else: 25 | pass 26 | 27 | print("Made it!") 28 | ` 29 | setupAndRunCode(t, script, "--color=never") 30 | assertOnlyOutput(t, stdOutBuffer, "Made it!\n") 31 | assertNoErrors(t) 32 | } 33 | 34 | func Test_Pass_ForLoop(t *testing.T) { 35 | script := ` 36 | for i in range(5): 37 | pass 38 | 39 | print("Made it!") 40 | ` 41 | setupAndRunCode(t, script, "--color=never") 42 | assertOnlyOutput(t, stdOutBuffer, "Made it!\n") 43 | assertNoErrors(t) 44 | } 45 | -------------------------------------------------------------------------------- /ci/install-rad.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Install Rad for CI usage 5 | # Downloads the latest release binary for Linux amd64 6 | 7 | INSTALL_DIR="/usr/local/bin" 8 | BINARY_NAME="rad" 9 | PLATFORM="linux_amd64" 10 | 11 | echo "🔽 Installing Rad for CI..." 12 | 13 | # Get the latest release URL 14 | LATEST_RELEASE_URL="https://github.com/amterp/rad/releases/latest/download/rad_${PLATFORM}.tar.gz" 15 | 16 | echo "📥 Downloading Rad binary from: $LATEST_RELEASE_URL" 17 | 18 | # Download and extract 19 | curl -fsSL "$LATEST_RELEASE_URL" | tar -xz --strip-components=0 -C /tmp 20 | 21 | # Move to install directory 22 | sudo mv "/tmp/${BINARY_NAME}" "${INSTALL_DIR}/${BINARY_NAME}" 23 | sudo chmod +x "${INSTALL_DIR}/${BINARY_NAME}" 24 | 25 | # Verify installation 26 | echo "✅ Rad installed successfully!" 27 | echo "📍 Location: ${INSTALL_DIR}/${BINARY_NAME}" 28 | echo "🔍 Version: $(rad -v)" -------------------------------------------------------------------------------- /core/testing/func_unique_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func TestUnique(t *testing.T) { 6 | script := ` 7 | print(unique([2, 1, 2, 3, 1, "Alice", 4, 3, 5, 5])) 8 | ` 9 | setupAndRunCode(t, script, "--color=never") 10 | assertOnlyOutput(t, stdOutBuffer, "[ 2, 1, 3, \"Alice\", 4, 5 ]\n") 11 | assertNoErrors(t) 12 | } 13 | 14 | func TestUnique_Large(t *testing.T) { 15 | script := ` 16 | a = unique([2 for i in range(1000)]) 17 | print(len(a)) 18 | print(a[0]) 19 | ` 20 | setupAndRunCode(t, script, "--color=never") 21 | assertOnlyOutput(t, stdOutBuffer, "1\n2\n") 22 | assertNoErrors(t) 23 | } 24 | 25 | func TestUnique_String(t *testing.T) { 26 | script := ` 27 | print(join(unique(split("Frodo Baggins is a hobbit", "")), "")) 28 | ` 29 | setupAndRunCode(t, script, "--color=never") 30 | assertOnlyOutput(t, stdOutBuffer, "Frod Baginshbt\n") 31 | assertNoErrors(t) 32 | } 33 | -------------------------------------------------------------------------------- /core/clock.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "time" 4 | 5 | type Clock interface { 6 | Now() time.Time 7 | Local() *time.Location 8 | } 9 | 10 | type RealClock struct { 11 | } 12 | 13 | func NewRealClock() Clock { 14 | return &RealClock{} 15 | } 16 | 17 | func (r *RealClock) Now() time.Time { 18 | return time.Now() 19 | } 20 | 21 | func (r *RealClock) Local() *time.Location { 22 | return time.Local 23 | } 24 | 25 | type FixedClock struct { 26 | NowTime time.Time 27 | } 28 | 29 | func NewFixedClock(year, month, day, hour, minute, second, nano int64, tz *time.Location) Clock { 30 | return &FixedClock{ 31 | NowTime: time.Date(int(year), time.Month(month), int(day), int(hour), int(minute), int(second), int(nano), tz), 32 | } 33 | } 34 | 35 | func (f *FixedClock) Now() time.Time { 36 | return f.NowTime 37 | } 38 | 39 | func (f *FixedClock) Local() *time.Location { 40 | return f.NowTime.Location() 41 | } 42 | -------------------------------------------------------------------------------- /core/testing/func_sum_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Func_Sum_Ints(t *testing.T) { 6 | script := ` 7 | a = [1, 2, 3] 8 | print(sum(a)) 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "6\n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func Test_Func_Sum_Mix(t *testing.T) { 16 | script := ` 17 | a = [1, 2.2, 3] 18 | print(sum(a)) 19 | ` 20 | setupAndRunCode(t, script, "--color=never") 21 | assertOnlyOutput(t, stdOutBuffer, "6.2\n") 22 | assertNoErrors(t) 23 | } 24 | 25 | func Test_Func_Sum_ErrorsForNonNumElements(t *testing.T) { 26 | script := ` 27 | a = [1, "ab", 3] 28 | print(sum(a)) 29 | ` 30 | setupAndRunCode(t, script, "--color=never") 31 | expected := `Error at L3:11 32 | 33 | print(sum(a)) 34 | ^ 35 | Value '[ 1, "ab", 3 ]' (list) is not compatible with expected type 'float[]' 36 | ` 37 | assertError(t, 1, expected) 38 | } 39 | -------------------------------------------------------------------------------- /lsp-server/analysis/diagnostics.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import ( 4 | "github.com/amterp/rad/lsp-server/log" 5 | "github.com/amterp/rad/lsp-server/lsp" 6 | 7 | "github.com/amterp/rad/rts/check" 8 | ) 9 | 10 | func (s *State) resolveDiagnostics(checker check.RadChecker) []lsp.Diagnostic { 11 | diagnostics := make([]lsp.Diagnostic, 0) 12 | 13 | result, err := checker.CheckDefault() 14 | if err == nil { 15 | s.addCheckerDiagnotics(&diagnostics, result) 16 | } else { 17 | log.L.Errorf("Failed to check script: %v", err) 18 | } 19 | return diagnostics 20 | } 21 | 22 | func (s *State) addCheckerDiagnotics(diagnostics *[]lsp.Diagnostic, checkResult check.Result) { 23 | checkDiagnostics := checkResult.Diagnostics 24 | 25 | log.L.Infof("Found %d checker diagnostics", len(checkDiagnostics)) 26 | 27 | for _, checkD := range checkDiagnostics { 28 | *diagnostics = append(*diagnostics, lsp.NewDiagnosticFromCheck(checkD)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rts/test/rts_query_test.go: -------------------------------------------------------------------------------- 1 | package rts_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/amterp/rad/rts" 7 | ) 8 | 9 | func Test_Tree_Query_CanFindStrings(t *testing.T) { 10 | radParser, _ := rts.NewRadParser() 11 | defer radParser.Close() 12 | 13 | script := `a = "hello" 14 | b = "there {1 + 1}" 15 | if true: 16 | c = "world!" 17 | ` 18 | tree := radParser.Parse(script) 19 | nodes, err := rts.QueryNodes[*rts.StringNode](tree) 20 | if err != nil { 21 | t.Fatalf("Query failed: %v", err) 22 | } 23 | 24 | if len(nodes) != 3 { 25 | t.Fatalf("Found %d nodes, expected 3", len(nodes)) 26 | } 27 | if nodes[0].Src() != "\"hello\"" { 28 | t.Fatalf("Node 0 src didn't match: <%v>", nodes[0].Src()) 29 | } 30 | if nodes[1].Src() != "\"there {1 + 1}\"" { 31 | t.Fatalf("Node 1 src didn't match: <%v>", nodes[1].Src()) 32 | } 33 | if nodes[2].Src() != "\"world!\"" { 34 | t.Fatalf("Node 2 src didn't match: <%v>", nodes[2].Src()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/testing/display_block_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_DisplayBlock_CanGiveOwnList(t *testing.T) { 6 | script := ` 7 | a = [ 8 | { 9 | "name": "alice" 10 | }, 11 | { 12 | "name": "bob" 13 | }, 14 | ] 15 | Name = json[].name 16 | display a: 17 | fields Name 18 | ` 19 | setupAndRunCode(t, script, "--color=never") 20 | expected := `Name 21 | alice 22 | bob 23 | ` 24 | assertOnlyOutput(t, stdOutBuffer, expected) 25 | assertNoErrors(t) 26 | } 27 | 28 | func Test_DisplayBlock_CanGiveOwnMap(t *testing.T) { 29 | script := ` 30 | a = { 31 | "results": [ 32 | { 33 | "name": "alice" 34 | }, 35 | { 36 | "name": "bob" 37 | }, 38 | ] 39 | } 40 | Name = json.results[].name 41 | display a: 42 | fields Name 43 | ` 44 | setupAndRunCode(t, script, "--color=never") 45 | expected := `Name 46 | alice 47 | bob 48 | ` 49 | assertOnlyOutput(t, stdOutBuffer, expected) 50 | assertNoErrors(t) 51 | } 52 | -------------------------------------------------------------------------------- /core/rad_time.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func NewTimeMap(time time.Time) *RadMap { 9 | timeMap := NewRadMap() 10 | hour := time.Hour() 11 | minute := time.Minute() 12 | second := time.Second() 13 | 14 | timeMap.SetPrimitiveStr("date", time.Format("2006-01-02")) 15 | timeMap.SetPrimitiveInt("year", time.Year()) 16 | timeMap.SetPrimitiveInt("month", int(time.Month())) 17 | timeMap.SetPrimitiveInt("day", time.Day()) 18 | timeMap.SetPrimitiveInt("hour", hour) 19 | timeMap.SetPrimitiveInt("minute", minute) 20 | timeMap.SetPrimitiveInt("second", second) 21 | timeMap.SetPrimitiveStr("time", fmt.Sprintf("%02d:%02d:%02d", hour, minute, second)) 22 | 23 | epochMap := NewRadMap() 24 | epochMap.SetPrimitiveInt64("seconds", time.Unix()) 25 | epochMap.SetPrimitiveInt64("millis", time.UnixMilli()) 26 | epochMap.SetPrimitiveInt64("nanos", time.UnixNano()) 27 | 28 | timeMap.SetPrimitiveMap("epoch", epochMap) 29 | 30 | return timeMap 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow for releasing Rad binaries 2 | # Uses goreleaser-cross Docker image for CGO cross-compilation 3 | # Publishes binaries for Linux, macOS, and Windows on multiple architectures 4 | 5 | name: Release 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | permissions: 13 | contents: write 14 | packages: write 15 | 16 | jobs: 17 | release: 18 | runs-on: ubuntu-latest 19 | container: 20 | image: goreleaser/goreleaser-cross:v1.21.5 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Set up Git 28 | run: | 29 | git config --global --add safe.directory /github/workspace 30 | git config --global --add safe.directory /__w/rad/rad 31 | 32 | - name: Run GoReleaser 33 | run: goreleaser release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /core/testing/file_header_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_FileHeader_PrintsOneLinerIfOnlyThat(t *testing.T) { 6 | script := ` 7 | --- 8 | This is a one liner! 9 | --- 10 | args: 11 | name str 12 | ` 13 | setupAndRunCode(t, script, "-h", "--color=never") 14 | expected := `This is a one liner! 15 | 16 | Usage: 17 | TestCase [OPTIONS] 18 | 19 | Script args: 20 | --name str 21 | ` 22 | assertOnlyOutput(t, stdOutBuffer, expected) 23 | assertNoErrors(t) 24 | } 25 | 26 | func Test_FileHeader_PrintsAll(t *testing.T) { 27 | script := ` 28 | --- 29 | This is a one liner! 30 | 31 | Here is 32 | the rest! 33 | --- 34 | args: 35 | name str 36 | ` 37 | setupAndRunCode(t, script, "-h", "--color=never") 38 | expected := `This is a one liner! 39 | 40 | Here is 41 | the rest! 42 | 43 | Usage: 44 | TestCase [OPTIONS] 45 | 46 | Script args: 47 | --name str 48 | ` 49 | assertOnlyOutput(t, stdOutBuffer, expected) 50 | assertNoErrors(t) 51 | } 52 | -------------------------------------------------------------------------------- /docs-web/docs/reference/logic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Boolean Logic 3 | --- 4 | 5 | ## Truthy / Falsy 6 | 7 | Rad supports truthy/falsy logic. 8 | 9 | For those unfamiliar, this means that, instead of writing the following (as an example): 10 | 11 | ```rad 12 | if len(my_list) > 0: 13 | print("My list has elements!") 14 | ``` 15 | 16 | you can write 17 | 18 | ```rad 19 | if my_list: 20 | print("My list has elements!") 21 | ``` 22 | 23 | Essentially, you can use any type as a condition, and it will resolve to true or false depending on the value. 24 | 25 | The following table shows which values return false for each type. **All other values resolve to true.** 26 | 27 | | Type | Falsy | Description | 28 | |-------|-------|---------------| 29 | | str | `""` | Empty strings | 30 | | int | `0` | Zero | 31 | | float | `0.0` | Zero | 32 | | list | `[]` | Empty lists | 33 | | map | `{}` | Empty maps | 34 | 35 | !!! note "" 36 | 37 | Note that a string which is all whitespace e.g. `" "` is truthy. 38 | -------------------------------------------------------------------------------- /lsp-server/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "go.uber.org/zap" 10 | "go.uber.org/zap/zapcore" 11 | ) 12 | 13 | var ( 14 | L *zap.SugaredLogger 15 | ) 16 | 17 | func InitLogger(w io.Writer) { 18 | logFilePath := filepath.Join(os.TempDir(), "rls.log") 19 | fmt.Fprintln(w, "Log file path: ", logFilePath) 20 | 21 | logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) 22 | if err != nil { 23 | panic("failed to open log file: " + err.Error()) 24 | } 25 | 26 | fileCore := zapcore.NewCore( 27 | zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), 28 | zapcore.AddSync(logFile), 29 | zapcore.InfoLevel, 30 | ) 31 | 32 | consoleCore := zapcore.NewCore( 33 | zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()), 34 | zapcore.AddSync(w), 35 | zapcore.InfoLevel, 36 | ) 37 | 38 | core := zapcore.NewTee(fileCore, consoleCore) 39 | 40 | logger := zap.New(core) 41 | L = logger.Sugar() 42 | 43 | defer L.Sync() 44 | } 45 | -------------------------------------------------------------------------------- /core/func_matches.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/amterp/rad/rts/rl" 7 | ) 8 | 9 | var FuncMatches = BuiltInFunc{ 10 | Name: FUNC_MATCHES, 11 | Execute: func(f FuncInvocation) RadValue { 12 | input := f.GetStr("_str").Plain() 13 | pattern := f.GetStr("_pattern").Plain() 14 | partial := f.GetBool("partial") 15 | 16 | re, err := regexp.Compile(pattern) 17 | if err != nil { 18 | return f.ReturnErrf(rl.ErrInvalidRegex, "Error compiling regex pattern: %s", err) 19 | } 20 | 21 | var matches bool 22 | if partial { 23 | matches = re.FindString(input) != "" 24 | } else { 25 | // anchoring pattern to ensure patterns like cat|dog get handled correctly 26 | anchoredPattern := "^(?:" + pattern + ")$" 27 | anchoredRe, err := regexp.Compile(anchoredPattern) 28 | if err != nil { 29 | return f.ReturnErrf(rl.ErrInvalidRegex, "Error compiling regex pattern: %s", err) 30 | } 31 | matches = anchoredRe.MatchString(input) 32 | } 33 | 34 | return f.Return(matches) 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /core/testing/func_len_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func TestLen_Array(t *testing.T) { 6 | script := ` 7 | a = [40, 50, 60] 8 | print(len(a)) 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "3\n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func TestLen_String(t *testing.T) { 16 | script := ` 17 | a = "alice" 18 | print(len(a)) 19 | ` 20 | setupAndRunCode(t, script, "--color=never") 21 | assertOnlyOutput(t, stdOutBuffer, "5\n") 22 | assertNoErrors(t) 23 | } 24 | 25 | func TestLen_EmojiString(t *testing.T) { 26 | script := ` 27 | a = "alice 👋" 28 | print(len(a)) 29 | ` 30 | setupAndRunCode(t, script, "--color=never") 31 | assertOnlyOutput(t, stdOutBuffer, "7\n") 32 | assertNoErrors(t) 33 | } 34 | 35 | func TestLen_Map(t *testing.T) { 36 | script := ` 37 | a = { "alice": 40, "bob": "bar", "charlie": [1, "hi"] } 38 | print(len(a)) 39 | ` 40 | setupAndRunCode(t, script, "--color=never") 41 | assertOnlyOutput(t, stdOutBuffer, "3\n") 42 | assertNoErrors(t) 43 | } 44 | -------------------------------------------------------------------------------- /core/type_error.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/amterp/rad/rts/rl" 7 | ts "github.com/tree-sitter/go-tree-sitter" 8 | ) 9 | 10 | type RadError struct { 11 | Node *ts.Node 12 | msg RadString 13 | Code rl.Error 14 | } 15 | 16 | func NewError(msg RadString) *RadError { 17 | return &RadError{ 18 | msg: msg, 19 | } 20 | } 21 | 22 | func NewErrorStrf(msg string, args ...interface{}) *RadError { // todo make a constructor forcing a Rad error code 23 | return &RadError{ 24 | msg: NewRadString(fmt.Sprintf(msg, args...)), 25 | } 26 | } 27 | 28 | func (e *RadError) SetCode(code rl.Error) *RadError { 29 | e.Code = code 30 | return e 31 | } 32 | 33 | func (e *RadError) SetNode(node *ts.Node) *RadError { 34 | e.Node = node 35 | return e 36 | } 37 | 38 | func (e *RadError) Msg() RadString { 39 | return e.msg 40 | } 41 | 42 | func (e *RadError) Equals(other *RadError) bool { 43 | return e.Msg().Equals(other.Msg()) 44 | } 45 | 46 | func (e *RadError) Hash() string { 47 | return e.Msg().Plain() 48 | } 49 | -------------------------------------------------------------------------------- /core/testing/func_count_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Count_Basic(t *testing.T) { 6 | script := ` 7 | print(count("banana", "n")) 8 | print(count("abracadabra", "a")) 9 | ` 10 | setupAndRunCode(t, script, "--color=never") 11 | assertOnlyOutput(t, stdOutBuffer, "2\n5\n") 12 | assertNoErrors(t) 13 | } 14 | 15 | func Test_Count_Overlap(t *testing.T) { 16 | script := ` 17 | print(count("aaa", "aa")) 18 | ` 19 | setupAndRunCode(t, script, "--color=never") 20 | assertOnlyOutput(t, stdOutBuffer, "1\n") 21 | assertNoErrors(t) 22 | } 23 | 24 | func Test_Count_Empty(t *testing.T) { 25 | script := ` 26 | print(count("aaa", "")) 27 | ` 28 | setupAndRunCode(t, script, "--color=never") 29 | assertOnlyOutput(t, stdOutBuffer, "4\n") 30 | assertNoErrors(t) 31 | } 32 | 33 | func Test_Count_EmptyStr(t *testing.T) { 34 | script := ` 35 | print(count("", "a")) 36 | print(count("", "")) 37 | ` 38 | setupAndRunCode(t, script, "--color=never") 39 | assertOnlyOutput(t, stdOutBuffer, "0\n1\n") 40 | assertNoErrors(t) 41 | } 42 | -------------------------------------------------------------------------------- /core/testing/tbl_misc_test.go: -------------------------------------------------------------------------------- 1 | package testing 2 | 3 | import "testing" 4 | 5 | func Test_Tbl_FillsMissingValuesWithEmptyStrings(t *testing.T) { 6 | script := ` 7 | names = ["Alice", "Bob", "Charlie"] 8 | ages = [25, 30] 9 | twice = [50, 60, 70] 10 | display: 11 | fields names, ages, twice 12 | ` 13 | setupAndRunCode(t, script, "--color=never") 14 | expected := `names ages twice 15 | Alice 25 50 16 | Bob 30 60 17 | Charlie 70 18 | ` 19 | assertOnlyOutput(t, stdOutBuffer, expected) 20 | assertNoErrors(t) 21 | } 22 | 23 | func Test_Tbl_FillsMissingValuesWithEmptyStringsShortestFirst(t *testing.T) { 24 | script := ` 25 | ages = [25, 30] 26 | names = ["Alice", "Bob", "Charlie"] 27 | twice = [50, 60, 70] 28 | display: 29 | fields ages, names, twice 30 | ` 31 | setupAndRunCode(t, script, "--color=never") 32 | expected := `ages names twice 33 | 25 Alice 50 34 | 30 Bob 60 35 | Charlie 70 36 | ` 37 | assertOnlyOutput(t, stdOutBuffer, expected) 38 | assertNoErrors(t) 39 | } 40 | -------------------------------------------------------------------------------- /rts/test/dumps/cases/args_repeat_unarys.dump: -------------------------------------------------------------------------------- 1 | ===== 2 | Args repeat unarys 3 | ===== 4 | args: 5 | age int = ++-+--30 6 | ===== 7 | B: [ 0, 29] PS: [0, 0] PE: [2, 0] source_file 8 | B: [ 0, 28] PS: [0, 0] PE: [1, 22] arg_block 9 | B: [ 0, 4] PS: [0, 0] PE: [0, 4] args `args` 10 | B: [ 4, 5] PS: [0, 4] PE: [0, 5] : `:` 11 | B: [10, 28] PS: [1, 4] PE: [1, 22] declaration: arg_declaration 12 | B: [10, 13] PS: [1, 4] PE: [1, 7] arg_name: identifier `age` 13 | B: [14, 17] PS: [1, 8] PE: [1, 11] type: int_type `int` 14 | B: [18, 19] PS: [1, 12] PE: [1, 13] = `=` 15 | B: [20, 28] PS: [1, 14] PE: [1, 22] default: int_arg 16 | B: [20, 21] PS: [1, 14] PE: [1, 15] op: + `+` 17 | B: [21, 22] PS: [1, 15] PE: [1, 16] op: + `+` 18 | B: [22, 23] PS: [1, 16] PE: [1, 17] op: - `-` 19 | B: [23, 24] PS: [1, 17] PE: [1, 18] op: + `+` 20 | B: [24, 25] PS: [1, 18] PE: [1, 19] op: - `-` 21 | B: [25, 26] PS: [1, 19] PE: [1, 20] op: - `-` 22 | B: [26, 28] PS: [1, 20] PE: [1, 22] value: int `30` 23 | -------------------------------------------------------------------------------- /docs/thinking/rad_check.md: -------------------------------------------------------------------------------- 1 | # Rad Check 2 | 3 | ## 2025-04-28 4 | 5 | We want a way to statically validate/check scripts without running them. For example, something like: 6 | 7 | ``` 8 | > rad check