├── 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 |
4 |
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 |
--------------------------------------------------------------------------------
/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 |
4 |
5 |
6 |
7 |
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