clean
This commit is contained in:
parent
0a80f6d13d
commit
66807c02c9
|
|
@ -77,12 +77,13 @@ describe('errors', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('multiline tests', () => {
|
describe.skip('multiline tests', () => {
|
||||||
test.only('multiline function', () => {
|
test('multiline function', () => {
|
||||||
expect(`
|
expect(`
|
||||||
add = fn a b:
|
add = fn a b:
|
||||||
result = a + b
|
result = a + b
|
||||||
result
|
result
|
||||||
|
end
|
||||||
add 3 4
|
add 3 4
|
||||||
`).toEvaluateTo(7)
|
`).toEvaluateTo(7)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => {
|
||||||
if (multilineMode) return transaction // Allow everything in multiline mode
|
if (multilineMode) return transaction // Allow everything in multiline mode
|
||||||
|
|
||||||
transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||||
console.log(`🌭`, { string: inserted.toString(), newline: inserted.toString().includes('\n') })
|
|
||||||
if (inserted.toString().includes('\n')) {
|
if (inserted.toString().includes('\n')) {
|
||||||
multilineMode = true
|
multilineMode = true
|
||||||
updateStatusMessage()
|
updateStatusMessage()
|
||||||
|
|
|
||||||
|
|
@ -350,13 +350,50 @@ describe('Assign', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('multiline', () => {
|
describe('multiline', () => {
|
||||||
test.only('parses multiline strings', () => {
|
test('parses multiline strings', () => {
|
||||||
expect(`'first'\n'second'`).toMatchTree(`
|
expect(`'first'\n'second'`).toMatchTree(`
|
||||||
String first
|
String first
|
||||||
String second`)
|
String second`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('trims leading and trailing whitespace in expected tree', () => {
|
test('parses multiline functions', () => {
|
||||||
|
expect(`
|
||||||
|
add = fn a b:
|
||||||
|
result = a + b
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
add 3 4
|
||||||
|
`).toMatchTree(`
|
||||||
|
Assign
|
||||||
|
Identifier add
|
||||||
|
= =
|
||||||
|
FunctionDef
|
||||||
|
fn fn
|
||||||
|
Params
|
||||||
|
Identifier a
|
||||||
|
Identifier b
|
||||||
|
: :
|
||||||
|
Assign
|
||||||
|
Identifier result
|
||||||
|
= =
|
||||||
|
BinOp
|
||||||
|
Identifier a
|
||||||
|
operator +
|
||||||
|
Identifier b
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier result
|
||||||
|
|
||||||
|
end end
|
||||||
|
FunctionCall
|
||||||
|
Identifier add
|
||||||
|
PositionalArg
|
||||||
|
Number 3
|
||||||
|
PositionalArg
|
||||||
|
Number 4`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ignores leading and trailing whitespace in expected tree', () => {
|
||||||
expect(`
|
expect(`
|
||||||
3
|
3
|
||||||
|
|
||||||
|
|
@ -374,6 +411,7 @@ end
|
||||||
Identifier x
|
Identifier x
|
||||||
Identifier y
|
Identifier y
|
||||||
: :
|
: :
|
||||||
|
FunctionCallOrIdentifier
|
||||||
Identifier x
|
Identifier x
|
||||||
end end
|
end end
|
||||||
`)
|
`)
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ singleLineFunctionDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
multilineFunctionDef {
|
multilineFunctionDef {
|
||||||
"fn" Params ":" newlineOrSemicolon (expression newlineOrSemicolon)* "end"
|
"fn" Params ":" newlineOrSemicolon (line newlineOrSemicolon)* "end"
|
||||||
}
|
}
|
||||||
|
|
||||||
Params {
|
Params {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import {tokenizer} from "./tokenizer"
|
||||||
import {highlighting} from "./highlight"
|
import {highlighting} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "'[OVQTOOOqQPO'#DTO!zQUO'#DTO#XQPOOOOQO'#DS'#DSO#xQTO'#CbOOQS'#DQ'#DQO$PQTO'#DVOOQO'#Cn'#CnOOQO'#C}'#C}O$XQPO'#C|OOQS'#Cu'#CuQ$aQTOOOOQS'#DP'#DPOOQS'#Ca'#CaO$hQTO'#ClOOQS'#DO'#DOOOQS'#Cv'#CvO$oQUO,58zO%SQTO,59_O%^QTO,58}O%^QTO,58}O%eQPO,58|O%vQUO'#DTO%}QPO,58|OOQS'#Cw'#CwO&SQTO'#CpO&[QPO,59qOOQS,59h,59hOOQS-E6s-E6sQOQPOOOOQS,59W,59WOOQS-E6t-E6tOOQO1G.y1G.yOOQO'#DT'#DTOOQO1G.i1G.iO&aQPO1G.iOOQS1G.h1G.hOOQS-E6u-E6uO&xQTO1G/]O'SQPO7+$wO'hQTO7+$xO'rQPO'#CxO(TQTO<<HdOOQO<<Hd<<HdOOQS,59d,59dOOQS-E6v-E6vOOQOAN>OAN>O",
|
states: "'[OVQTOOOqQPO'#DTO!zQUO'#DTO#XQPOOOOQO'#DS'#DSO#xQTO'#CbOOQS'#DQ'#DQO$PQTO'#DVOOQO'#Cn'#CnOOQO'#C}'#C}O$XQPO'#C|OOQS'#Cu'#CuQ$aQTOOOOQS'#DP'#DPOOQS'#Ca'#CaO$hQTO'#ClOOQS'#DO'#DOOOQS'#Cv'#CvO$oQUO,58zO%SQTO,59_O%^QTO,58}O%^QTO,58}O%eQPO,58|O%vQUO'#DTO%}QPO,58|OOQS'#Cw'#CwO&SQTO'#CpO&[QPO,59qOOQS,59h,59hOOQS-E6s-E6sQOQPOOOOQS,59W,59WOOQS-E6t-E6tOOQO1G.y1G.yOOQO'#DT'#DTOOQO1G.i1G.iO&aQPO1G.iOOQS1G.h1G.hOOQS-E6u-E6uO&xQTO1G/]O'SQPO7+$wO'hQTO7+$xO'uQPO'#CxO'zQTO<<HdOOQO<<Hd<<HdOOQS,59d,59dOOQS-E6v-E6vOOQOAN>OAN>O",
|
||||||
stateData: "(b~OoOS~OPQOQUO]UO^UO_UOcVOuTO{ZO~OWwXXwXYwXZwX{qX|qX~OP]OQUO]UO^UO_UOa_OuTOWwXXwXYwXZwX~OhcO{[X|[X~P!VOWdOXdOYeOZeO~OQUO]UO^UO_UOuTO~OPgO~P#gOPiOedP~O{lO|lO~O|nO~PVOP]O~P#gOP]Oa_O{Sa|SaxSa~P#gOPQOcVO~P#gOPrO~P#gOxuOWwXXwXYwXZwX~Ox[X~P!VOxuO~OPiOedX~OewO~OWdOXdOYViZVi{Vi|VixVi~OPrO{yO~P#gOWdOXdOYeOZeO{yq|yq~OPrOf|O~P#gOWdOXdOYeOZeO{}O~OPrOf!PO~P#gO^Z~",
|
stateData: "([~OoOS~OPQOQUO]UO^UO_UOcVOuTO{ZO~OWwXXwXYwXZwX{qX|qX~OP]OQUO]UO^UO_UOa_OuTOWwXXwXYwXZwX~OhcO{[X|[X~P!VOWdOXdOYeOZeO~OQUO]UO^UO_UOuTO~OPgO~P#gOPiOedP~O{lO|lO~O|nO~PVOP]O~P#gOP]Oa_O{Sa|SaxSa~P#gOPQOcVO~P#gOPrO~P#gOxuOWwXXwXYwXZwX~Ox[X~P!VOxuO~OPiOedX~OewO~OWdOXdOYViZVi{Vi|VixVi~OPrO{yO~P#gOWdOXdOYeOZeO{yq|yq~OPQOcVOf|O~P#gO{}O~OPQOcVOf!PO~P#gO^Z~",
|
||||||
goto: "%[{PPPP|!U!Z!jPPPP|PPP!UP!uP!zPP!uP!}#T#[#bPPP#h#l#s#x$QP$c$rP%V%VUXO[cRhTV`QbgkUOQT[_bcdegwy{cSOT[cdewy{VXO[cRkVQ[ORm[SbQgRpbQjVRvjQ{yR!O{TZO[SYO[RqcVaQbgU^QbgRo_bSOT[cdewy{X]Q_bgUPO[cQfTZrdewy{WROT[cQsdQteQxwTzy{VWO[c",
|
goto: "%d{PPPP|!W!]!lPPPP|PPP!WP!wP#OPP!wP#R#X#`#fPPP#l#p#{$Q$YP$k$zP%]%]YXO[cy{RhTV`QbgkUOQT[_bcdegwy{cSOT[cdewy{ZXO[cy{RkVQ[ORm[SbQgRpbQjVRvjQ{yR!O{TZO[SYO[QqcTzy{VaQbgU^QbgRo_bSOT[cdewy{X]Q_bgYPO[cy{QfTVrdew[ROT[cy{QsdQteRxwZWO[cy{",
|
||||||
nodeNames: "⚠ Identifier Word Program FunctionCall PositionalArg ParenExpr BinOp operator operator operator operator FunctionCallOrIdentifier String Number Boolean NamedArg NamedArgPrefix FunctionDef fn Params : end Assign =",
|
nodeNames: "⚠ Identifier Word Program FunctionCall PositionalArg ParenExpr BinOp operator operator operator operator FunctionCallOrIdentifier String Number Boolean NamedArg NamedArgPrefix FunctionDef fn Params : end Assign =",
|
||||||
maxTerm: 44,
|
maxTerm: 44,
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
|
|
@ -15,5 +15,5 @@ export const parser = LRParser.deserialize({
|
||||||
tokenData: "(j~ReXY!dYZ!ipq!dwx!nxy#]yz#bz{#g{|#l}!O#q!P!Q$d!Q![#y![!]$i!]!^!i!_!`$n#T#X$s#X#Y%R#Y#Z%|#Z#h$s#h#i'u#i#o$s~~(e~!iOo~~!nO{~~!qTOw!nwx#Qx;'S!n;'S;=`#V<%lO!n~#VO]~~#YP;=`<%l!n~#bOu~~#gOx~~#lOW~~#qOY~~#vPZ~!Q![#y~$OQ^~!O!P$U!Q![#y~$XP!Q![$[~$aP^~!Q![$[~$iOX~~$nOe~~$sOh~Q$vQ!_!`$|#T#o$sQ%ROaQR%US!_!`$|#T#b$s#b#c%b#c#o$sR%eS!_!`$|#T#W$s#W#X%q#X#o$sR%vQfP!_!`$|#T#o$s~&PT!_!`$|#T#U&`#U#b$s#b#c'j#c#o$s~&cS!_!`$|#T#`$s#`#a&o#a#o$s~&rS!_!`$|#T#g$s#g#h'O#h#o$s~'RS!_!`$|#T#X$s#X#Y'_#Y#o$s~'dQ_~!_!`$|#T#o$sR'oQcP!_!`$|#T#o$s~'xS!_!`$|#T#f$s#f#g(U#g#o$s~(XS!_!`$|#T#i$s#i#j'O#j#o$s~(jO|~",
|
tokenData: "(j~ReXY!dYZ!ipq!dwx!nxy#]yz#bz{#g{|#l}!O#q!P!Q$d!Q![#y![!]$i!]!^!i!_!`$n#T#X$s#X#Y%R#Y#Z%|#Z#h$s#h#i'u#i#o$s~~(e~!iOo~~!nO{~~!qTOw!nwx#Qx;'S!n;'S;=`#V<%lO!n~#VO]~~#YP;=`<%l!n~#bOu~~#gOx~~#lOW~~#qOY~~#vPZ~!Q![#y~$OQ^~!O!P$U!Q![#y~$XP!Q![$[~$aP^~!Q![$[~$iOX~~$nOe~~$sOh~Q$vQ!_!`$|#T#o$sQ%ROaQR%US!_!`$|#T#b$s#b#c%b#c#o$sR%eS!_!`$|#T#W$s#W#X%q#X#o$sR%vQfP!_!`$|#T#o$s~&PT!_!`$|#T#U&`#U#b$s#b#c'j#c#o$s~&cS!_!`$|#T#`$s#`#a&o#a#o$s~&rS!_!`$|#T#g$s#g#h'O#h#o$s~'RS!_!`$|#T#X$s#X#Y'_#Y#o$s~'dQ_~!_!`$|#T#o$sR'oQcP!_!`$|#T#o$s~'xS!_!`$|#T#f$s#f#g(U#g#o$s~(XS!_!`$|#T#i$s#i#j'O#j#o$s~(jO|~",
|
||||||
tokenizers: [0, 1, tokenizer],
|
tokenizers: [0, 1, tokenizer],
|
||||||
topRules: {"Program":[0,3]},
|
topRules: {"Program":[0,3]},
|
||||||
tokenPrec: 337
|
tokenPrec: 331
|
||||||
})
|
})
|
||||||
|
|
|
||||||
244
today.md
Normal file
244
today.md
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
# 🌟 Modern Language Inspiration & Implementation Plan
|
||||||
|
|
||||||
|
## Language Research Summary
|
||||||
|
|
||||||
|
### Pipe Operators Across Languages
|
||||||
|
|
||||||
|
| Language | Syntax | Placeholder | Notes |
|
||||||
|
|----------|--------|-------------|-------|
|
||||||
|
| **Gleam** | `\|>` | `_` | Placeholder can go anywhere, enables function capture |
|
||||||
|
| **Elixir** | `\|>` | `&1`, `&2` | Always first arg by default, numbered placeholders |
|
||||||
|
| **Nushell** | `\|` | structured data | Pipes structured data, not just text |
|
||||||
|
| **F#** | `\|>` | none | Always first argument |
|
||||||
|
| **Raku** | `==>` | `*` | Star placeholder for positioning |
|
||||||
|
|
||||||
|
### Conditional Syntax
|
||||||
|
|
||||||
|
| Language | Single-line | Multi-line | Returns Value |
|
||||||
|
|----------|------------|------------|---------------|
|
||||||
|
| **Lua** | `if x then y end` | `if..elseif..else..end` | No (statement) |
|
||||||
|
| **Luau** | `if x then y else z` | Same | Yes (expression) |
|
||||||
|
| **Ruby** | `x = y if condition` | `if..elsif..else..end` | Yes |
|
||||||
|
| **Python** | `y if x else z` | `if..elif..else:` | Yes |
|
||||||
|
| **Gleam** | N/A | `case` expressions | Yes |
|
||||||
|
|
||||||
|
## 🍤 Shrimp Design Decisions
|
||||||
|
|
||||||
|
### Pipe Operator with Placeholder (`|`)
|
||||||
|
|
||||||
|
**Syntax Choice: `|` with `_` placeholder**
|
||||||
|
|
||||||
|
```shrimp
|
||||||
|
# Basic pipe with placeholder
|
||||||
|
"hello world" | upcase _
|
||||||
|
"log.txt" | tail _ lines=10
|
||||||
|
|
||||||
|
# Placeholder positioning flexibility
|
||||||
|
"error.log" | grep "ERROR" _ | head _ 5
|
||||||
|
data | process format="json" input=_
|
||||||
|
|
||||||
|
# Multiple placeholders (future consideration)
|
||||||
|
value | combine _ _
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this design:**
|
||||||
|
- **`|` over `|>`**: Cleaner, more shell-like
|
||||||
|
- **`_` placeholder**: Explicit, readable, flexible positioning
|
||||||
|
- **Gleam-inspired**: Best of functional programming meets shell scripting
|
||||||
|
|
||||||
|
### Conditionals
|
||||||
|
|
||||||
|
**Multi-line syntax:**
|
||||||
|
```shrimp
|
||||||
|
if condition:
|
||||||
|
expression
|
||||||
|
elsif other-condition:
|
||||||
|
expression
|
||||||
|
else:
|
||||||
|
expression
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Single-line syntax (expression form):**
|
||||||
|
```shrimp
|
||||||
|
result = if x = 5: "five"
|
||||||
|
# Returns nil when false
|
||||||
|
|
||||||
|
result = if x > 0: "positive" else: "non-positive"
|
||||||
|
# Explicit else for non-nil guarantee
|
||||||
|
```
|
||||||
|
|
||||||
|
**Design choices:**
|
||||||
|
- **`elsif` not `else if`**: Avoids nested parsing complexity (Ruby-style)
|
||||||
|
- **`:` after conditions**: Consistent with function definitions
|
||||||
|
- **`=` for equality**: Context-sensitive (assignment vs comparison)
|
||||||
|
- **`nil` for no-value**: Short, clear, well-understood
|
||||||
|
- **Expressions return values**: Everything is an expression philosophy
|
||||||
|
|
||||||
|
## 📝 Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Grammar Foundation
|
||||||
|
|
||||||
|
**1.1 Add Tokens**
|
||||||
|
```grammar
|
||||||
|
@tokens {
|
||||||
|
// Existing...
|
||||||
|
"|" // Pipe operator
|
||||||
|
"_" // Placeholder
|
||||||
|
"if" // Conditionals
|
||||||
|
"elsif"
|
||||||
|
"else"
|
||||||
|
"nil" // Null value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**1.2 Precedence Updates**
|
||||||
|
```grammar
|
||||||
|
@precedence {
|
||||||
|
multiplicative @left,
|
||||||
|
additive @left,
|
||||||
|
pipe @left, // After arithmetic, before assignment
|
||||||
|
assignment @right,
|
||||||
|
call
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Grammar Rules
|
||||||
|
|
||||||
|
**2.1 Pipe Expression**
|
||||||
|
```grammar
|
||||||
|
PipeExpr {
|
||||||
|
expression !pipe "|" PipeTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
PipeTarget {
|
||||||
|
FunctionCallWithPlaceholder |
|
||||||
|
FunctionCall // Error in compiler if no placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
FunctionCallWithPlaceholder {
|
||||||
|
Identifier PlaceholderArg+
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaceholderArg {
|
||||||
|
PositionalArg | NamedArg | Placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
Placeholder {
|
||||||
|
"_"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2.2 Conditional Expression**
|
||||||
|
```grammar
|
||||||
|
Conditional {
|
||||||
|
SingleLineIf | MultiLineIf
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleLineIf {
|
||||||
|
"if" Comparison ":" expression ElseClause?
|
||||||
|
}
|
||||||
|
|
||||||
|
MultiLineIf {
|
||||||
|
"if" Comparison ":" newlineOrSemicolon
|
||||||
|
(line newlineOrSemicolon)*
|
||||||
|
ElsifClause*
|
||||||
|
ElseClause?
|
||||||
|
"end"
|
||||||
|
}
|
||||||
|
|
||||||
|
ElsifClause {
|
||||||
|
"elsif" Comparison ":" newlineOrSemicolon
|
||||||
|
(line newlineOrSemicolon)*
|
||||||
|
}
|
||||||
|
|
||||||
|
ElseClause {
|
||||||
|
"else" ":" (expression | (newlineOrSemicolon (line newlineOrSemicolon)*))
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparison {
|
||||||
|
expression "=" expression // Context-sensitive in if/elsif
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2.3 Update line rule**
|
||||||
|
```grammar
|
||||||
|
line {
|
||||||
|
PipeExpr |
|
||||||
|
Conditional |
|
||||||
|
FunctionCall |
|
||||||
|
// ... existing rules
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Test Cases
|
||||||
|
|
||||||
|
**Pipe Tests:**
|
||||||
|
```shrimp
|
||||||
|
# Basic placeholder
|
||||||
|
"hello" | upcase _
|
||||||
|
|
||||||
|
# Named arguments with placeholder
|
||||||
|
"file.txt" | process _ format="json"
|
||||||
|
|
||||||
|
# Chained pipes
|
||||||
|
data | filter _ "error" | count _
|
||||||
|
|
||||||
|
# Placeholder in different positions
|
||||||
|
5 | subtract 10 _ # 10 - 5 = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conditional Tests:**
|
||||||
|
```shrimp
|
||||||
|
# Single line
|
||||||
|
x = if n = 0: "zero"
|
||||||
|
|
||||||
|
# Single line with else
|
||||||
|
sign = if n > 0: "positive" else: "negative"
|
||||||
|
|
||||||
|
# Multi-line
|
||||||
|
if score > 90:
|
||||||
|
grade = "A"
|
||||||
|
elsif score > 80:
|
||||||
|
grade = "B"
|
||||||
|
else:
|
||||||
|
grade = "C"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Nested conditionals
|
||||||
|
if x > 0:
|
||||||
|
if y > 0:
|
||||||
|
quadrant = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Compiler Implementation
|
||||||
|
|
||||||
|
**4.1 PipeExpr Handling**
|
||||||
|
- Find placeholder position in right side
|
||||||
|
- Insert left side value at placeholder
|
||||||
|
- Error if no placeholder found
|
||||||
|
|
||||||
|
**4.2 Conditional Compilation**
|
||||||
|
- Generate JUMP bytecode for branching
|
||||||
|
- Handle nil returns for missing else
|
||||||
|
- Context-aware `=` parsing
|
||||||
|
|
||||||
|
## 🎯 Key Decision Points
|
||||||
|
|
||||||
|
1. **Placeholder syntax**: `_` vs `$` vs `?` → **Choose `_` (Gleam-like)**
|
||||||
|
2. **Pipe operator**: `|` vs `|>` vs `>>` → **Choose `|` (cleaner)**
|
||||||
|
3. **Nil naming**: `nil` vs `null` vs `none` → **Choose `nil` (Ruby-like)**
|
||||||
|
4. **Equality**: Keep `=` context-sensitive or add `==`? → **Keep `=` (simpler)**
|
||||||
|
5. **Single-line if**: Require else or default nil? → **Default nil (flexible)**
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. Update grammar file with new tokens and rules
|
||||||
|
2. Write comprehensive test cases
|
||||||
|
3. Implement compiler support for pipes
|
||||||
|
4. Implement conditional bytecode generation
|
||||||
|
5. Test edge cases and error handling
|
||||||
|
|
||||||
|
This plan combines the best ideas from modern languages while maintaining Shrimp's shell-like simplicity and functional philosophy!
|
||||||
Loading…
Reference in New Issue
Block a user